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");
}

7 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Can you do "Contains" queries? Can you create a method that allows you to pass in a Filter string?

    ReplyDelete
  3. Tim,

    1. Yes, Contains works and builds an or filter against all the contents of the collection/array.

    2. Right now there's no way to pass in a filter yourself, but I do have class for building equal or not equal filters:

    u => Filter.Equal(u, "attribute name", "attribute value")

    This will build an equal filter for whatever attribute name and value you used so you can still query against attributes that are not mapped.

    ReplyDelete
  4. Tim,

    I just checked in a way to specify your own filters. I created an extension method called FilterWith (I'll probably end up changing the name) that allows you to pass in your own filter so you can do something like this:

    context.Query().FilterWith("(SAmAccountName=ahatter)").ToList();

    I hope this is what you were thinking.

    ReplyDelete
  5. That's awesome. Basically I'm running into situations where I need to add AND or OR filters dynamically.

    Kind of gets me stuck with building my own query string.

    ReplyDelete
  6. I've been in that boat before. I was glad to be able to add FilterWith. I also support dynamic expressions using two methods I got from Predicate Builder. I'll cover them in my upcoming Query post. I should have it up tomorrow or Saturday.

    ReplyDelete
  7. That's great! I just ran across the PredicateBuilder class, adding support for that to LinqToLdap makes this a pretty robust finshed product.

    Looking forward to seeing documentation on it!

    ReplyDelete