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!