Sunday, February 20, 2011

LINQ To LDAP: How Does It Work Part 3

Time to query! I'll continue working with my User class that created in Part 2. So how can you query?

Where
You can chain any number of where clauses together. For instance these two methods will produce the same filter (not exactly the same):
var where = context.Query<User>()
    .Where(u => u.FirstName == "Sir" && 
                u.LastName == "Phobos" && 
                u.Title == "Knight of Mars");
//produces: (&(objectCategory=Person)(&(givenname=Sir)(sn=Phobos)(Title=Knight of Mars)))

var where = context.Query<User>()
    .Where(u => u.FirstName == "Sir")
    .Where(u => u.LastName == "Phobos")
    .Where(u => u.Title == "Knight of Mars");
//produces: (&(objectCategory=Person)(givenname=Sir)(sn=Phobos)(Title=Knight of Mars))
In the future I will try to work on improving the number of nested ANDs that get created.

First / Single / Any / Count
These are immediate execution methods. I support all of these including the "OrDefault" variations. Single() and SingleOrDefault() will throw exceptions accordingly. First() will not throw an exception if there are no elements in the collection so it behaves the same as FirstOrDefault(). When you use either Any() or Count(), they both make use of LDAP paging controls. If the server does not support paging, then it will fail. You can use any of these methods in combination with Where and Select. However, if you use Select then order does matter depending on the Select projection. For instance:
//this works
var where = context.Query<User>()
    .Where(u => u.FirstName == "Alan")
    .Select(u => new User{FirstName = u.FirstName, LastName = u.LastName})
    .FirstOrDefault(u => u.LastName == "Hatter");

//this works because the property names on the anonymous projection match the mapped class.
var where = context.Query<User>()
    .Where(u => u.FirstName == "Alan")
    .Select(u => new {FirstName = u.FirstName, LastName = u.LastName})
    .FirstOrDefault(u => u.LastName == "Hatter");

//this will fail because the property name Last does not match LastName
var where = context.Query<User>()
    .Where(u => u.FirstName == "Alan")
    .Select(u => new User{FirstName = u.FirstName, Last = u.LastName})
    .FirstOrDefault(u => u.Last == "Hatter");

//produces: (&(objectCategory=Person)(givenname=Alan)(sn=Hatter))

ListAttributes, ListServerAttributes and Custom Filters
I blogged about ListAttributes in my earlier post "Attributes, Where?" so I won't cover it again. ListServerAttributes is a variation of ListAttributes that allows you to see information about the LDAP server.

FilterWith is a method I added to allow you to specify your own filters when querying. You can even mix FilterWith with other methods.
var filterWith = context.Query<User>()
    .FilterWith("&(givenname=Sir)(sn=Phobos)")
    .Select(u => u)
    .ToList();

//produces: (&(objectCategory=Person)(&(givenname=Sir)(sn=Phobos)))

var filterWith = context.Query<User>()
    .Where(u => u.Title == "Knight of Mars")
    .FilterWith("&(givenname=Sir)(sn=Phobos)");

//produces: (&(&(objectCategory=Person)(Title=Knight of Mars))(&(givenname=Sir)(sn=Phobos)))

I also created a static class that allows you to mix searching mapped properties and unmapped properties:
var query = context.Query<User>()
    .Where(u => u.FirstName == "Alan" && 
                Filter.Equal(u, "sn", "Hatter"));

//produces: (&(objectCategory=Person)(&(givenname=Alan)(sn=Hatter)))

var query = context.Query<User>()
    .Where(u => !(u.FirstName == "Alan" && 
                  Filter.Approximately(u, "sn", "Hatter")));
//produces: (&(objectCategory=Person)(&(!(givenname=Alan))(!(sn~=Hatter))))

Query Semantics
Let's start with looking for everyone that fits one of these criteria: Sales rep or manager, named Andrew Fuller, not in the USA, manager for Anne Dodsworth or last name starting with Hatter. Also, all of them must have had an account created before 11/24/2010.
var annesDn = context.Query<User>()
    .Where(u => u.Name == "Anne Dodsworth")
    .Select(u => u.DistinguishedName)
    .FirstOrDefault();

var titles = new List<string> {"Sales Representative", "Sales Manager"};
            
DateTime date = new DateTime(2010, 11, 24);
            
var complex = context.Query<User>()
    .Where(u => u.WhenCreated < date && 
                (titles.Contains(u.Title) ||
                 u.Name == "Andrew Fuller" ||
                 u.Country != "USA" ||
                 u.Employees.Contains(annesDn) ||
                 u.LastName.StartsWith("Hatter")));

//produces this monster: (&(objectCategory=Person)(&(WhenCreated<=20101124000000.0Z)(!(WhenCreated=20101124000000.0Z))(|(|(Title=Sales Representative)(Title=Sales Manager))(Name=Andrew Fuller)(!(c=USA))(directreports=CN=Anne Dodsworth,CN=Users,CN=Employees,DC=Northwind,DC=local)(sn=Hatter*))))

Now let's look for no one with a title or people with the phone number (206) 555-3412:
var complex = context.Query<User>()
    .Where(u => u.Title == null || u.PhoneNumber == "(206) 555-3412");

//produces (&(objectCategory=Person)(|(!(Title=*))(telephonenumber=\\28206\\29 555-3412)))
Null gets converted to a "not anything" value and the parentheses require special escape characters so that's why the phone number looks different in the filter. I filter '\', '*', '(', ')', and '\u0000' for all filter values except when using the FilterWith method.

And now searching by guid:
var complex = context.Query<User>()
    .Where(u => u.Guid == new Guid("f6c3a6b4-9197-4c33-8565-7dbd27cb8bb2"));

//produces (&(objectCategory=Person)(objectguid=\\b4\\a6\\c3\\f6\\97\\91\\33\\4c\\85\\65\\7d\\bd\\27\\cb\\8b\\b2))

I also want to cover a class I added based on PredicateBuilder for building dynamic expressions. I adapted one of their examples to show how you can dynamically build expressions:
var inner = PredicateBuilder.Create<User>();
inner = inner.Or(p => p.Title.Contains("foo"));
inner = inner.Or(p => p.Title.Contains("far"));

var outer = PredicateBuilder.Create<User>();
outer = outer.And(p => p.EmployeeId >= 100);
outer = outer.And(p => p.EmployeeId <= 1000);
outer = outer.And(inner);

var query = context.Query<User>()
    .Where(outer);

//produces (&(objectCategory=Person)(&(EmployeeId>=100)(EmployeeId<=1000)(|(Title=*foo*)(Title=*far*))))

And here's the PredicateBuilder class in LINQ to LDAP:
public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> Create<T>()
    {
        return default(Expression<Func<T, bool>>);
    }

    public static Expression<Func<T, TResult>> CreateExpression<T, TResult>(
        this T example, Expression<Func<T, TResult>> expression)
    {
        return expression;
    }

    public static Expression<Func<T, bool>> Or<T>(
         this Expression<Func<T, bool>> sourceExpression,
         Expression<Func<T, bool>> appendExpression)
    {
        if (sourceExpression == null)
        {
            return appendExpression;
        }
        var invokedExpr = Expression.Invoke(appendExpression, 
            sourceExpression.Parameters);
        return Expression.Lambda<Func<T, bool>>(
            Expression.OrElse(sourceExpression.Body, invokedExpr), 
            sourceExpression.Parameters);
    }

    public static Expression<Func<T, bool>> And<T>(
         this Expression<Func<T, bool>> sourceExpression,
         Expression<Func<T, bool>> appendExpression)
    {
        if (sourceExpression == null)
        {
            return appendExpression;
        }
        var invokedExpr = Expression.Invoke(appendExpression, 
            sourceExpression.Parameters);
        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(sourceExpression.Body, invokedExpr), 
            sourceExpression.Parameters);
    }
}

Something seems to be missing...
You may be wondering why aggreagate functions, joins, etc. support is missing. Basically LDAP is not relational. Implementing this in any way would require client side evaluation and re-querying which is beyond me to implement right now. OrderBy is missing because I didn't discover the SortRequestControl until recently so look for it in a later release (there will be one since I want to add paging and asynchronous support).

On a side note I wanted to mention that I support logging queries through a Log property on the DirectoryContext. You can also see the filter produced without executing the query against a directory by using ToString.
using (var context = new DirectoryContext() { Log = Console.Out })
{
    var query = context.Query<User>()
        .Where(u => u.FirstName == "Alan");

    //Doesn't actually query the directory
    var str = query.ToString();
}

So that's it for now. I hope that's a pretty good starter for how to use LINQ to LDAP. Next stop: v1.0! It'll be over at codeplex when I put together some documentation. Until then you're welcome to build the project yourself. It uses VS 2010 and .Net 4.

Wednesday, February 9, 2011

LINQ To LDAP: How Does It Work Part 2.6

This will be a quick post. I know I said my next post would be about querying, but I completely forgot about attribute based mapping so I want to cover that first.

So here's my class from before but now with attributes:
[DirectorySchema("CN=Users,CN=Employees,DC=Northwind,DC=local", "Person")]
public class User
{
    [DirectoryAttribute]
    public string DistinguishedName { get; set; }

    [DirectoryAttribute("badpwdcount")]
    public int BadPasswordCount { get; set; }

    [DirectoryAttribute("directreports")]
    public string[] Employees { get; set; }

    [DirectoryAttribute]
    public string Title { get; set; }

    [DirectoryAttribute]
    public string PostalCode { get; set; }

    [DirectoryAttribute("cn")]
    public string CommonName { get; set; }

    [DirectoryAttribute]
    public DateTime? WhenCreated { get; set; }

    [DirectoryAttribute("givenname")]
    public string FirstName { get; set; }

    [DirectoryAttribute("objectguid")]
    public Guid Guid { get; set; }

    [DirectoryAttribute("l")]
    public string City { get; set; }

    [DirectoryAttribute("usnchanged")]
    public int Version { get; set; }

    [DirectoryAttribute("c")]
    public string Country { get; set; }

    [DirectoryAttribute("whenchanged")]
    public DateTime? LastChanged { get; set; }

    [DirectoryAttribute("objectsid")]
    public byte[] Sid { get; set; }

    [DirectoryAttribute("employeeid")]
    public long EmployeeId { get; set; }

    [DirectoryAttribute("telephonenumber")]
    public string PhoneNumber { get; set; }

    [DirectoryAttribute]
    public string Street { get; set; }

    [DirectoryAttribute]
    public string Comment { get; set; }

    [DirectoryAttribute]
    public string Name { get; set; }

    [DirectoryAttribute("sn")]
    public string LastName { get; set; }
}

And here's how I added support:
public static bool HasDirectorySchema(this Type type)
{
    var attributes = type.GetCustomAttributes(typeof (DirectorySchemaAttribute), true);
    return attributes != null && attributes.Length > 0;
}

internal static IObjectMapping Map<T>(string namingContext, string objectCategory) where T : class
{
    lock (Mappings)
    {
        var mapping = GetMapping<T>() ??
                        (typeof (T).HasDirectorySchema()
                            ? Map(new AttributeClassMap<T>())
                            : Map(new AutoClassMap<T>(namingContext, objectCategory)));

        return mapping;
    }
}

public class AttributeClassMap<T> : ClassMap<T> where T : class 
{
    public AttributeClassMap()
    {
        var type = typeof(T);
        var schemaAttribute = type
            .GetCustomAttributes(typeof(DirectorySchemaAttribute), true)
            .Cast<DirectorySchemaAttribute>().First();

        NamingContext(schemaAttribute.NamingContext);
        ObjectCategory(schemaAttribute.ObjectCategory);

            
        var properties = type.GetProperties(Flags)
            .Where(p => p.GetCustomAttributes(typeof(DirectoryAttributeAttribute), true).Any() &&
                        p.GetGetMethod() != null && p.GetSetMethod() != null)
            .Select(p => 
                    new KeyValuePair<DirectoryAttributeAttribute, PropertyInfo>(
                        p.GetCustomAttributes(typeof (DirectoryAttributeAttribute), true)
                             .Cast<DirectoryAttributeAttribute>().FirstOrDefault(), 
                        p));

        properties.ToList().ForEach(p => Map(p.Value).Named(p.Key.AttributeName));
    }
}

Attribute mapping is kind of the bridge between auto-mapping and class mapping. You can query like with class mapping, but without creating a separate class that maps your properties.
using (var context = new DirectoryContext())
{
    var user = context.Query<User>()
        .FirstOrDefault(u => u.CommonName == "Alan Hatter");
}

Monday, February 7, 2011

LINQ To LDAP: How Does It Work Part 2.5

This is a continuation of my last post here. I showed how you can manually map classes in that post. Now I want to show how you wire it all up.

I created a class for fluent configuration called LdapConfiguration. This is what it looks like right now:

public class LdapConfiguration : IConnectionFactory
{
    private readonly string _serverName;
    private int _port;
    private bool _fullyQualifiedDnsHostName;
    private bool _useUdp;
    private AuthType? _authType;
    private NetworkCredential _credentials;

    internal static IConnectionFactory ConnectionFactory;

    /// <summary>
    /// Begins the fluent configration for connecting to an LDAP server.
    /// </summary>
    /// <param name="serverName">
    /// The server name can be an IP address,  a DNS domain or host name.
    /// </param>
    public LdapConfiguration(string serverName)
    {
        if (string.IsNullOrWhiteSpace(serverName)) 
            throw new ArgumentNullException("serverName");

        ConnectionFactory = this;
        _serverName = serverName;
        _port = 389;
    }

    /// <summary>
    /// Sets the port manually for the LDAP server.  The default is 389.
    /// </summary>
    /// <param name="port">
    /// The port to use when communicating with the LDAP server.
    /// </param>
    /// <returns></returns>
    public LdapConfiguration UsePort(int port)
    {
        _port = port;
        return this;
    }

    /// <summary>
    /// Sets the port to the default SSL port for the LDAP server.  The default is 636.
    /// </summary>
    /// <returns></returns>
    public LdapConfiguration UseSsl()
    {
        _port = 636;
        return this;
    }

    /// <summary>
    /// Sets the port to the default Global Catalog port for Active Directory 
    /// based LDAP servers.  The default is 3268.
    /// </summary>
    /// <returns></returns>
    public LdapConfiguration UseGlobalCatalog()
    {
        _port = 3268;
        return this;
    }

    /// <summary>
    /// If this option is called, the server name is a fully-qualified DNS host name. 
    /// Otherwise the server name can be an IP address, a DNS domain or host name.
    /// </summary>
    /// <returns></returns>
    public LdapConfiguration ServerNameIsFullyQualified()
    {
        _fullyQualifiedDnsHostName = true;
        return this;
    }

    /// <summary>
    /// Indicates that the connections will use UDP (User Datagram Protocol).
    /// </summary>
    /// <returns></returns>
    public LdapConfiguration UseUdp()
    {
        _useUdp = true;
        return this;
    }

    /// <summary>
    /// Allows you to specify an authentication method for the 
    /// connection.  If this method is not called,  the authentication method 
    /// will be resolved by the <see cref="LdapConnection"/>.
    /// </summary>
    /// <param name="authType">
    /// The type of authentication to use.
    /// </param>
    /// <returns></returns>
    public LdapConfiguration AuthenticateBy(AuthType authType)
    {
        _authType = authType;
        return this;
    }

    /// <summary>
    /// Allows you to specify credentials for the conneciton to use.  
    /// If this method is not called,  then the <see cref="LdapConnection"/> 
    /// will use the credentials of the current user.
    /// </summary>
    /// <param name="credentials">
    /// The credentials to use.
    /// </param>
    /// <returns></returns>
    public LdapConfiguration AuthenticateAs(NetworkCredential credentials)
    {
        _credentials = credentials;
        return this;
    }

    /// <summary>
    /// Adds the mapping for querying.
    /// </summary>
    /// <param name="classMap">The mapping for the class</param>
    /// <returns></returns>
    public LdapConfiguration AddMapping(IClassMap classMap)
    {
        DirectoryMapper.Map(classMap);
        return this;
    }

    /// <summary>
    /// Adds all mappings in the assembly.
    /// </summary>
    /// <param name="assembly">
    /// The assembly containing all of the mappings
    /// </param>
    /// <returns></returns>
    public LdapConfiguration AddMappingsFrom(Assembly assembly)
    {
        foreach (var type in assembly.GetTypes())
        {
            if (type.IsInterface) continue;

            var baseType = type.BaseType;
            while (baseType != typeof(object))
            {
                if (baseType.IsGenericType && 
                    baseType.GetGenericTypeDefinition() == typeof(ClassMap<>))
                {
                    AddMapping(Activator.CreateInstance(type) as IClassMap);
                    break;
                }
                baseType = baseType.BaseType;
            }
        }

        return this;
    }

    /// <summary>
    /// Adds all mappings in the assembly.
    /// </summary>
    /// <param name="assemblyName">
    /// The assembly containing all of the mappings
    /// </param>
    /// <returns></returns>
    public LdapConfiguration AddMappingsFrom(string assemblyName)
    {
        if (string.IsNullOrWhiteSpace(assemblyName)) 
            throw new ArgumentNullException("assemblyName");

        assemblyName = assemblyName.ToLower().EndsWith(".dll")
                            ? assemblyName
                            : assemblyName + ".dll";

        var assembly = Assembly.LoadFrom(assemblyName);

        return AddMappingsFrom(assembly);
    }

    /// <summary>
    /// Provides access to all currently mapped classes.
    /// </summary>
    /// <returns></returns>
    public static ReadOnlyDictionary<Type, IObjectMapping> GetMappings()
    {
        return DirectoryMapper.GetMappings();
    }

    LdapConnection IConnectionFactory.GetConnection()
    {
        return GetConnection();
    }

    /// <summary>
    /// Allows access for controlling connection creation and lifetime.
    /// </summary>
    /// <returns></returns>
    protected virtual LdapConnection GetConnection()
    {
        var identifier = new LdapDirectoryIdentifier(_serverName, _port, 
            _fullyQualifiedDnsHostName, _useUdp);

        return _authType.HasValue
            ? new LdapConnection(identifier, _credentials, _authType.Value)
            : new LdapConnection(identifier, _credentials);
    }
}

Here's a basic example for configuring an Active Directory based configuration:
new LdapConfiguration("myserver.com")
    .UseGlobalCatalog()
    .AuthenticateBy(AuthType.Negotiate)
    .AddMappingsFrom("some.other.assembly")
    .AddMapping(new UserMapping());

So what's this doing?
  • UseGlobalCatalog sets the port for connecting to a global catalog server in an AD Forrest. See here for more information.
  • AuthenticateBy sets the authentication method for communicating with the server. This will always default to Negotiate (DirectoryServices.Protocols makes that decision) so most of the time you won't have to set this value. In my basic experience with non-AD LDAP servers, I imagine you'll be setting this and AuthenticateAs more often.
  • AddMappingFrom allows you to map all of the classes that subtype ClassMap in a given assembly. Pretty useful if you have a bunch of mappings.
  • AddMapping let's you add them one at a time or exclude some.

You only need to configure this once so put it in your start up code and you're good to go. Whenever you create a DirectoryContext now you don't need to pass in a connection:
using (var context = new DirectoryContext())
{
    var user = context.Query<User>()
        .FirstOrDefault(u => u.CommonName == "Alan Hatter");
}

And here's what goes on behind the scenes:
public DirectoryContext()
{
    if (LdapConfiguration.ConnectionFactory == null) 
        throw new MappingException("No configuration has been provided.  Please use LinqToLdap.Mapping.LdapConfiguration.");

    _disposeOfConnection = true;
    _connection = LdapConfiguration.ConnectionFactory.GetConnection();
}

I think I'll have one more post on querying and everything should be ready for a 1.0 release. WOOO!

Sunday, February 6, 2011

LINQ to LDAP: Attributes, Where?

I think one of the hardest things for me in working with LDAP is finding out what is actually in the directory. A Google search can help with identifying predefined attributes, but what about custom ones? Each directory is going to be different and different organizations are going to track different things. I wanted to make this discovery process easier.

I created an extension method called ListAttributes that does what it's name suggests. I'll use LINQPad to show how much I like this feature. You can download it here for free.


First thing you'll want to do is Launch the application and switch the language to "C# Program"


Right click and then select "Query Properties"


You should now see this screen. From here you should click on "Add" to add System.DirectoryServices.Protocols and "Browse" to add LinqToLdap.dll from where you have it on your machine.


It should look like this when you're done.


Next click the "Additional Namespace Imports" tab and type in System.DirectoryServices.Protocols and LinqToLdap.

After you hit OK, you can then type in some C# code and execute it. I'm going to use Andrew Fuller from the Northwind database that I used to populate my directory.

void Main()
{
 using (LdapConnection connection = new LdapConnection("localhost"))
 {
  var example = new { Cn = "" };
  using (var context = new DirectoryContext(connection))
  {
   context.Query(example, "CN=Employees,DC=Northwind,DC=local")
    .Where(u => u.Cn == "Andrew Fuller")
    .ListAttributes()
    .Dump();
  }
 }
}

That Dump() method is a special extension method that is part of LINQPad. It will format the results auto-magically for you which is why I like it so much. Here's what I get when I execute the query:


Now this won't just dump everything. It will only list populated attributes. It's not perfect, but it will give you a pretty good idea what is and isn't tracked about directory objects and what their data types are. LINQPad is even doing some color coding for you to show you what's a byte[]. Awesome!

Tuesday, February 1, 2011

LINQ To LDAP: How Does It Work Part 2

So up until this point all of my posts have used the auto-mapping via anonymous types. For this post I want to cover manual mapping of classes.

Here is an example mapping:

public class User
{
    public string DistinguishedName { get; set; }
    public int BadPasswordCount { get; set; }
    public string[] Employees { get; set; }
    public string Title { get; set; }
    public string PostalCode { get; set; }
    public string CommonName { get; set; }
    public DateTime? WhenCreated { get; set; }
    public string FirstName { get; set; }
    public Guid Guid { get; set; }
    public string City { get; set; }
    public int Version { get; set; }
    public string Country { get; set; }
    public DateTime? LastChanged { get; set; }
    public byte[] Sid { get; set; }
    public long EmployeeId { get; set; }
    public string PhoneNumber { get; set; }
    public string Street { get; set; }
    public string Comment { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

public class UserMapping : ClassMap<User>
{
    public UserMapping()
    {
        NamingContext("CN=Users,CN=Employees,DC=Northwind,DC=local");
        ObjectCategory("person");

        Map(u => u.BadPasswordCount).Named("badpwdcount");
        Map(u => u.Employees).Named("directreports");
        Map(u => u.Title);
        Map(u => u.PostalCode);
        Map(u => u.CommonName).Named("cn");
        Map(u => u.WhenCreated);
        Map(u => u.FirstName).Named("givenname");
        Map(u => u.Guid).Named("objectguid");
        Map(u => u.City).Named("l");
        Map(u => u.Version).Named("usnchanged");
        Map(u => u.Country).Named("c");
        Map(u => u.LastChanged).Named("whenchanged");
        Map(u => u.Sid).Named("objectsid");
        Map(u => u.DistinguishedName);
        Map(u => u.EmployeeId);
        Map(u => u.PhoneNumber).Named("telephonenumber");
        Map(u => u.Street);
        Map(u => u.Comment);
        Map(u => u.Name);
        Map(u => u.LastName).Named("sn");
    }
}

I specify the object's location in the directory by using NamingContext(). I also map it to a object category, but it's not required. Mapping properties is pretty straightforward. You use a lambda expression to map a property and if it has a different name in the directory then you can specify that with Named().

It's not always necessary to create a specific mapping for your classes. The same way auto-mapping works with anonymous types, you can use the class as an example and it will map everything by convention. You have two options if you want to auto-map an existing class:

//Option 1
var user = context.Query<User>("CN=Users,CN=Employees,DC=Northwind,DC=local")
                .FirstOrDefault(u => u.Name == "Alan Hatter");

//Option 2
var example = new User();
var user = context.Query(example, "CN=Users,CN=Employees,DC=Northwind,DC=local", "Person")
                .FirstOrDefault(u => u.Name == "Alan Hatter");

If you try to auto-map an existing class without specifying a namingContext, you will get a MappingException. Also, remember your property names have to match the names in the directory for everything to work with auto-mapping. I'll cover finding available attributes, their types, and names in my next post. Stay tuned!