Saturday, April 07, 2007 6:31 PM bart

The IQueryable tales - LINQ to LDAP - Part 3: Why do we need entities?

Introduction

Welcome back to the LINQ-to-LDAP series. So far, we've been discussing:

In the previous post, we discussed quite a bit pieces of the LINQ puzzle, focusing in much detail on expression trees and the role of IQueryable<T> as a query provider's opportunity to parse an expression tree. As you saw, the CreateQuery method plays a central role in this story. In this and future posts, we'll zoom in to this CreateQuery method by creating a first (simple) implementation that makes a translation to LDAP queries. But before we can do so, a bit of preparation needs to be done: enter the world of LDAP query syntax and entity types.

 

Refreshing LDAP queries

In order to be on the same page, let's refresh our LDAP query knowledge. In RFC terms, "LDAP queries" are known as "search filters", which are specified in RFC 2254 entitled "The String Representation of LDAP Search Filters". Although it has been obsoleted by RFC 4515: "Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters" in June 2006, we'll stick with RFC 2254 as implemented by Active Directory (see Active Directory's LDAP Compliance, How Active Directory Searches Work and Search Filter Syntax).

The ABNF grammar of search filters is displayed below, in a slightly restructured format for easier comsumption:

filter = "(" filtercomp ")"
filterlist = 1*filter

filtercomp = and / or / not / item

  • and = "&" filterlist
  • or = "|" filterlist
  • not = "!" filter
  • item = simple / present / substring / extensible
    • simple = attr filtertype value
      • attr = AttributeDescription from Section 4.1.5 of RFC 2251
      • filtertype = equal / approx / greater / less
        • equal = "="
        • approx = "~="
        • greater = ">="
        • less = "<="
      • value = AttributeValue from Section 4.1.6 of RFC 2251
    • present = attr "=*"
      • attr = AttributeDescription from Section 4.1.5 of RFC 2251
    • substring = attr "=" [initial] any [final]
      • initial = value
        • value = AttributeValue from Section 4.1.6 of RFC 2251
      • any = "*" *(value "*")
        • value = AttributeValue from Section 4.1.6 of RFC 2251
      • final = value
        • value = AttributeValue from Section 4.1.6 of RFC 2251
    • extensible = attr [":dn"] [":" matchingrule] ":=" value / [":dn"] ":" matchingrule ":=" value
      • attr = AttributeDescription from Section 4.1.5 of RFC 2251
      • matchingrule = MatchingRuleId from Section 4.1.9 of RFC 2251
      • value = AttributeValue from Section 4.1.6 of RFC 2251

An example of a production is the following:

  • filter = "(" filtercomp ")"
    • filtercomp = item
      • item = simple
        • simple = attr filtertype value
          • attr = givenName
          • filtertype = equal
            • equal = "="
          • value = Bart

resulting in

   givenName=Bart

Other samples of queries are listed below:

  • (cn=Babs Jensen)
  • (!(cn=Tim Howes))
  • (&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))
  • (o=univ*of*mich*)

LDAP queries have a prefix-operator notation; humans (and therefore C# programmers) are more familiar with infix notation. These queries are equivalent to the following in C#-ish style:

  • cn == "Babs Jensen"
  • cn != "Tim Howes"
  • objectClass == "Person" && (sn == "Jensen" || cn == "Babs J*")
  • o == "univ*of*mich*"

However, prefix notation is slightly easier to deal with during parsing. Take a look at my DynCalc post series for more information, especially on Infix to Postfix (the inverse of prefix).

For sake of simplicity we trim the grammar down to the following:

filter = "(" filtercomp ")"
filterlist = 1*filter

filtercomp = and / or / not / item

  • and = "&" filterlist
  • or = "|" filterlist
  • not = "!" filter
  • item = simple / present / substring
    • simple = attr filtertype value
      • attr = AttributeDescription from Section 4.1.5 of RFC 2251
      • filtertype = equal / greater / less
        • equal = "="
        • greater = ">="
        • less = "<="
      • value = AttributeValue from Section 4.1.6 of RFC 2251
    • present = attr "=*"
      • attr = AttributeDescription from Section 4.1.5 of RFC 2251
    • substring = attr "=" [initial] any [final]
      • initial = value
        • value = AttributeValue from Section 4.1.6 of RFC 2251
      • any = "*" *(value "*")
        • value = AttributeValue from Section 4.1.6 of RFC 2251
      • final = value
        • value = AttributeValue from Section 4.1.6 of RFC 2251

 

The curse of the Impedance Mismatch

LINQ is all about overcoming the impedance mismatch that exists between various data models, such as hierarchical (XML) or relational (SQL) versus objects (OO). Again, with LDAP, we're faced with such a mismatch of data model representations, in this case directory objects (AD) versus objects (OO). As the matter in fact, objects kept in directory services like AD (Active Directory) or ADAM (Active Directory/Application Mode) have a strong-typed fashion when exposed to the outside world; for example take a look at IADsUser. Beside of a series of properties (like FirstName) those objects also support operations via methods (like SetPassword). This brings us to the first question: "How to map .NET objects to query objects?".

Recall that our LINQ queries will be executed against an object that implements IQueryable<T>. In our case, T has to be some kind of "entity class" (senso lato). The properties of this particular object will play a prominent role because the way the compiler works; as the matter in fact LINQ enforces strong-typing and type safety inside the query. An example to clarify things:

1 class User 2 { 3 public string FirstName { get; set; } 4 public string LastName { get; set; } 5 public int Age { get; set; } 6 } 7 8 class Program 9 { 10 static void Main() 11 { 12 IQueryable<User> src = ...; 13 var res = from usr in src 14 where usr.FirstName.StartsWith("B") && usr.Age >= 24 15 select usr.FirstName + " " + usr.LastName; 16 } 17 }

An example of type safety in the sample above is the variable usr which is of type User throughout the whole query. Because of this, the use of properties can be validated, as well as their types (e.g. Age is an Int32, FirstName is a string). Therefore, we need a good mapping mechanism by means of "entity types" that represent the objects we're querying for.

Things get a little more complicated though: "What about query operators?" The query from the fragment above doesn't translate very well to LDAP. If you've read the How Active Directory Searches Work article in detail, you've noticed that LDAP operators for <= and >= perform lexicographical comparisons. For simplicity's sake however, we won't pay much attention to this caveat. Similarly, we won't provider support for the ~= operator. Furthermore, LDAP is a very very basic language that is far from complete from a query's perspective; therefore we won't be able to translate operations like sorting, grouping, etc into LDAP. Queries like the one shown below:

1 DemoDataContext ctx = new DemoDataContext(); 2 ctx.Log = Console.Out; 3 var res = (from usr in ctx.Users 4 where usr.Age >= 24 5 orderby usr.Name descending 6 select usr.Name).Skip(10).Take(5); 7 foreach (var u in res) 8 ;

translate easily into SQL statements, as illustrated below:

but there's no counterpart in LDAP. A solution to this is to split queries into a piece that does get translated into LDAP while remaining pieces are executed on the client machine as LINQ-to-Objects queries using System.Linq.Enumerable. Such conversions are made possible using the AsEnumerable and AsQueryable methods respectively:

1 DemoDataContext ctx = new DemoDataContext(); 2 ctx.Log = Console.Out; 3 var res = (from usr in ctx.Users 4 where usr.Age >= 24 5 orderby usr.Name descending 6 select usr.Name).AsEnumerable().Skip(10).Take(5); 7 foreach (var u in res) 8 ;

In here, the first part of the query is represented as an expression tree, while the last part with the Skip(10).Take(5) calls is applied on an IEnumerable<string> object, resulting in direct compilation to executable code. The resulting query sent to the database looks like this:

Notice that the SQL plumbing around "paging" with Skip-Take isn't present. Note: if you'd only use a Take(n) method call, you'd get a SELECT TOP n * FROM ... SQL query:

For example, if the underlying datasource (like AD) doesn't support sorting, one could overcome this issue using the following query:

1 var res = (from usr in users 2 where usr.Name.StartsWith("B") 3 select usr.Name).AsEnumerable().OrderByDescending(name => name);

It's less elegant, but the piece between parentheses can be translated into LDAP without any problems (something like (givenName=B*) would be a good translation, see further), while the rest of the query is left to LINQ-to-Objects via the AsEnumerable() call. Notice that the projection in the last line results in an IQueryable<string>, therefore AsEnumerable will return an IEnumerable<string> which on its turn acts as a datasource for LINQ-to-Objects. With this source, we have only string instances left, thus the lambda expression for OrderByDescending needs to be name => name (or s => s or ...).

Another query is shown below and was split into pieces, making the distinction between the IQueryable and IEnumerable portions more sharp:

1 var t = (from usr in users 2 where usr.Name.StartsWith("B") 3 select usr).AsEnumerable(); 4 var res = (from usr in t 5 where usr.Age >= 24 6 orderby usr.Age descending 7 select usr.Name);

We also go rid of the explicit OrderByDescending call by using a second portion of LINQ syntax. One could write this in one statement too:

1 var res = from usr in 2 (from usr in users 3 where usr.Name.StartsWith("B") 4 select usr).AsEnumerable() 5 where usr.Age >= 24 6 orderby usr.Age descending 7 select usr.Name;

As the matter in fact, your IQueryable<T> implementation could be smart enough to track all the things it can't do directly in LDAP (for instance the sorting operations) and send off an LDAP query to AD with the maximum amount of things it can do using LDAP. Once it has fetched the results, all the things it wasn't able to do could be performed by compiling the remainder of the query to Enumerable.* calls (LINQ-to-Objects) and sending the results from the LDAP query through this in-memory query pipeline automatically. This would drive us too far from home, so we'll stick with a simple implementation.

Another question we need to answer is "How method calls are mapped into queries?". Method calls go much beyond the ones you see in the query language itself, such as OrderBy, GroupBy, ThenBy, Take, Skip, ... Other much useful ones are applied on the (common) data types of a query themselves, such as the System.String methods. Consider a LINQ-to-SQL query like the one below:

var res = from usr in ctx.Users where usr.Name.Substring(1, 3).ToLower().Replace("a", "b").CompareTo("c") > 0 && usr.Age + 1 >= 24 && (DateTime.Now - usr.Birthday).TotalDays % 100 == 0 select usr;

agains a table defined like this:

with ID = int, Name = nvarchar(50), Age = int, Birthday = datetime. If you can predict the SQL query that was generated by the LINQ-to-SQL implementation, you deserve a statue in the "LINQ Hall of Fame". Indeed, everything happens on the server using this autogenerated (at runtime!) query:

SELECT [t0].[ID], [t0].[Name], [t0].[Age], [t0].[Birthday]
FROM [dbo].[Users] AS [t0]
WHERE (REPLACE(LOWER(SUBSTRING([t0].[Name], @p0 + 1, @p1)), @p2, @p3) > @p4) AND (([t0].[Age] + @p5) >= @p6) AND ((((CONVERT(Float,CONVERTBigInt,(((CONVERT(BigInt,DATEDIFF(DAY, [t0].[Birthday], @p7))) * 86400000) + DATEDIFF(MILLISECOND, DATEADD(DAY, DATEDIFF(DAY, [t0].Birthday], @p7), [t0].[Birthday]), @p7)) * 10000))) / 864000000000) % @p8) = @p9)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) NOT NULL [1]
-- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) NOT NULL [3]
-- @p2: Input NVarChar (Size = 1; Prec = 0; Scale = 0) NOT NULL [a]
-- @p3: Input NVarChar (Size = 1; Prec = 0; Scale = 0) NOT NULL [b]
-- @p4: Input NVarChar (Size = 1; Prec = 0; Scale = 0) NOT NULL [c]
-- @p5: Input Int (Size = 0; Prec = 0; Scale = 0) NOT NULL [1]
-- @p6: Input Int (Size = 0; Prec = 0; Scale = 0) NOT NULL [24]
-- @p7: Input DateTime (Size = 0; Prec = 0; Scale = 0) NOT NULL [4/7/2007 10:45:17 AM]
-- @p8: Input Float (Size = 0; Prec = 0; Scale = 0) NOT NULL [100]
-- @p9: Input Float (Size = 0; Prec = 0; Scale = 0) NOT NULL [0]

So, if you want to create a damn good query provider to some underlying data source, that exploits all of that datasource's querying capabilities you're up for a lot of work. In the sample above, the SQL functions REPLACE, LOWER, SUBSTRING, CONVERT, DATEDIFF, DATEADD were used which only represent a small subset of the available functions. Notice that DateTime.Now was treated as a constant that was supplied as a parameter (@p7) in order to overcome clock differences and skews between the machine running the query and the machine executing it (SQL Server). Even things like Math.Sin are translated into corresponding SQL Server functions!

Maybe it's a good exercise to try to get LINQ-to-SQL on its knees by writing overly complex queries that use a bunch of methods from various BCL types :-). That said, there are things that LINQ-to-SQL doesn't know how to deal with. I cheated a bit by extending System.String with my own extension method:

A simple System.String extension method - Copy Code
1 static class Ext 2 { 3 public static string Reverse(this string s) 4 { 5 char[] sa = s.ToCharArray(); 6 Array.Reverse(sa); 7 return new string(sa); 8 } 9 }

When launching a query like the following one:

Invalid LINQ-to-SQL query - Copy Code
1 var res = from usr in ctx.Users 2 where usr.Name.Reverse() == "traB" 3 select usr; 4 5 foreach (var u in res) 6 ;

LINQ-to-SQL kindly admits its own mortality by throwing a NotSupportedException:

Unhandled Exception: System.NotSupportedException: Method 'System.String Reverse(System.String)' has no supported translation to SQL.

It's clear that we won't focus on supporting all sorts of exotic methods. We do want to support a few common ones though, especially those that prove useful in writing (implicit) wildcard queries. Three typical ones are listed below:

A few queries with "implicit wildcards" - Copy Code
1 var res1 = from usr in users where usr.Name.StartsWith("B") select usr; 2 var res2 = from usr in users where usr.Name.EndsWith("t") select usr; 3 var res3 = from usr in users where usr.Name.Contains("ar") select usr;

These get translated into the following LDAP queries respectively:

1 (&(objectClass=user)(givenName=B*))
2 (&(objectClass=user)(givenName=*t))
3 (&(objectClass=user)(givenName=*ar*))

Careful readers with eye for detail will notice that the (objectClass=user) portion comes out of the blue all of a sudden. Furthermore, the usr.Name property has been translated into givenName too. These differences will become clear when talking about "entity objects" and mapping schemes.

To wrap up, one should be aware of the limitations of our implementation:

  • No support for advanced query operations since LDAP doesn't support these. Simple from ... where ... select ... statements are supported though.
  • No support for the ~= operator.
  • Limited string operation support to allow implicit wildcard queries.
  • Semantics of >= and <= might be counterintuitive, causing a mismatch between OO and LDAP.

 

We need entities!

In the previous paragraph, we touched the question of entity types already: "How to map .NET objects to query objects?". End-users only care about one thing: an easy way to write queries that are (strongly-typed and) integrated with the language, in the LINQ philosophy. To do this, we need a type to be passed in as a type parameter (T) to some IQueryable<T> implementation. To set your mind, think back of LINQ-to-SQL:

  1. In Visual Studio "Orcas", a LINQ to SQL file is made:


  2. Next, tables and/or sprocs are dragged-and-dropped from the Server Explorer to the Object Relational Designer surface:


  3. Finally, queries can be written like this:

    Copy Code
    1 NorthwindDataContext ctx = new NorthwindDataContext(); 2 3 var res = from usr in ctx.Users 4 where usr.Name == "Bart" 5 select usr;

Behind the scenes, the designer created the NorthwindDataContext class that has the following signature:

public partial class NorthwindDataContext : global::System.Data.Linq.DataContext {


In here, we have a property called Users, declared like this:

public global::System.Data.Linq.Table<User> Users { get { return this.GetTable<User>(); } }


In here, User is the entity type, representing a row from the source table:

Finally, the Table<User> represents the queryable table and indeed, it implements IQueryable<User>:

public sealed class Table<T> : IQueryable<T>, IEnumerable<T>, ITable, IQueryable, IEnumerable, IListSource

Back to LINQ-to-LDAP now. First, we'll also need a class that plays a similar role to Table<T>. It needs to be IQueryable<T> and it should represent a data source coming from AD. In all of my creativity, I came up with the name DirectorySource<T>:

 

Next, the to-be-queried object types need to be represented in some way or another, by means of entities. These are simple class definitions that need to carry enough information too cook up an LDAP query. This is where things get a little tricky. Why? Let's take a look at some of the contents of Active Directory by means of the Windows Server 2003 Support Tool called ldp.exe:

This output shows the details for a user called "Bart De Smet" in the "Demo" organizational unit (OU) in my domain linqdemo.local. All of the lines in the right-hand side pane represent individual properties. For example, my sn is "Bart De Smet" while my physicalDeliveryOfficeName is "Test". These are just two samples of the naming schema employed by AD (and other directory services out there in the wild). Instead, I'd like to talk to AD with more familiar terms like LastName (instead of sn) and Office (instead of physicalDeliveryOfficeName). Therefore, we need some mapping mechanism that makes our entity class "natural" to the users, like this:

A first attempted entity type - Copy Code
1 public class User 2 { 3 public string FirstName { get; set; } 4 public string LastName { get; set; } 5 public string Office { get; set; } 6 }

so that I can write queries like this:

Natural feeling query against AD - Copy Code
1 DirectorySource<User> users = new DirectorySource<User>(...); 2 var res = from usr in users 3 where usr.Office == "Building 10 - 1.25" 4 select usr.FirstName + " " + usr.LastName;

In order to express this mapping, metadata turns out handy. Therefore, we'll create a custom attribute that can be applied to properties (I won't allow public fields since automatic properties make the creation of properties in C# 3.0 plain easy) and that contains information about the mapping to the underlying "internal name" used in Active Directory. Let's just show what I mean based on a revised entity type definition:

A better entity type with mappings - Copy Code
1 public class User 2 { 3 [DirectoryAttribute("givenName")] 4 public string FirstName { get; set; } 5 6 [DirectoryAttribute("sn")] 7 public string LastName { get; set; } 8 9 [DirectoryAttribute("physicalDeliveryOfficeName")] 10 public string Office { get; set; } 11 } 12

Using (an optional) attribute, we've added information to the properties needed to do the mapping. The class itself hasn't changed, so the query mentioned above remains perfectly valid. There's still one other thing we need to add to the class's metadata however: the underlying directory object type (or class schema) that we're going to query. To add this piece of information, we'll apply an attribute on the class level, like this:

An outstanding entity type - Copy Code
1 [DirectorySchema("user")] 2 public class User 3 { 4 [DirectoryAttribute("givenName")] 5 public string FirstName { get; set; } 6 7 [DirectoryAttribute("sn")] 8 public string LastName { get; set; } 9 10 [DirectoryAttribute("physicalDeliveryOfficeName")] 11 public string Office { get; set; } 12 }

Now take a look back at our original query. Based on the the custom attributes metadata, our DirectorySource<T> class is capable of making the translation into LDAP. By now you should be able to see the translation by visual inspection; here it is:

(&(objectClass=user)(physicalDeliveryOfficeName=Building 10 - 1.25))

The purple portion originates from the DirectorySchema attribute value, while the green portion originates from the DirectoryAttribute applied to the Office property on lines 10-11.

Note: You might wonder how you can easily grab all of the properties from the AD schema you're querying for. This is a more than valid concern indeed. One way is to use the ldp.exe tool to inspect current entries in the system. However, it does only display the attributes that are supplied for a given directory entry (for example the properties set for an individual user account). If you want to see the schema itself, you can use a tool called ADSI Edit that comes with Windows Server 2003 and is available through the MMC snap-ins:

If you configure it properly

you'll be able to inspect the schema in detail. Below you can see a screenshot for the User class schema:

If you open it, you can find a bunch of interesting information. For example, there's an entry called systemMayContain with optional attributes (contrast to systemMustContain):

Another property that will interest you is the subClassOf property that reveals the class hierarchy:

The hierarchy for user is: User <-- Organizational-Person <-- Person <-- Top. Top is the root type (like IUnknown or System.Object if you want). This way, you'll find that the physicalDeliveryOffice attribute is used in Organizational-Person. Furthermore, each attribute is also defined in the schema in order to get the required type information, a "required" indicator (nullability), etc. For example, you'll find an attribute called Physical-Delivery-Office-Name:

I agree this is a tiresome job to do; that's why I've created a little tool called AdMetal that allows to export the schema and walk the inheritance tree automatically. Output for the Organizational-Person object is shown in the screenshot below:

You can download an early alpha of this tool over here. Ultimately, this tool could become capable of much more, like automatic code generation for entity types, much like the O/R designer for LINQ-to-SQL. Time will tell... But for now, consider it as a handy tool to query the schema easily. The names between parentheses are the ones you need in the DirectoryAttribute and DirectorySchema attributes, aka the "LDAP display names".

The DirectoryAttributes are not just there to serve the predicate-formulating phase of the LDAP translation however. You might have noticed that LDAP doesn't have a projection portion; indeed LDAP queries are nothing more than filter predicates ("where" clauses). However, the libraries on top of it have support for some kind of projection. I'm referring to the System.DirectoryServices.DirectoryEntry class that has a Properties collection, as well as - more importantly - the System.DirectoryServices.DirectorySearcher class that is used to process queries. In order to execute our LDAP query, we'd write the following piece of code:

A classic LDAP query from C# - Copy Code
1 IEnumerable<string> GetResults() 2 { 3 DirectoryEntry root = new DirectoryEntry("LDAP://localhost/DC=linqdemo,DC=local"); 4 string query = "(&(objectClass=user)(physicalDeliveryOffice=Building 10 - 1.25))"; 5 string[] properties = new string[] { "givenName", "sn" }; 6 7 DirectorySearcher search = new DirectorySearcher(root, query, properties, SearchScope.Subtree); 8 SearchResultCollection res = search.FindAll(); 9 10 foreach (SearchResult sr in res) 11 { 12 DirectoryEntry e = sr.GetDirectoryEntry(); 13 yield return (string)e.Properties["givenName"][0] + " " + (string)e.Properties["sn"][0]; 14 } 15 }

This is the mechanical equivalent of our wanna-be-LINQ-query. Take a look at line 5, where the desired properties (the "projection") are listed as the propertiesToLoad (see constructor information). These values come back in line 13 where the projection really happens (in here using manual coding though). Based on this observation, you can conclude that our IQueryable<T> implementation should figure out the "properties to load" required to do the projection. Think about this for a while; it might seem trivial but it certainly isn't. Consider the following query to get this insight:

Wanna see a complex query? - Copy Code
1 var res = from usr in users 2 where usr.Name.StartsWith("A") 3 select new { Nick = GetNickName(usr.GivenName + " " + usr.LastName), 4 Stats = new { TwiceLogonCount = usr.LogonCount * 2, usr.Office.ToLower() } };

Don't ask why you'd write such a query, but be assured that your customers will try it! In here, the projection on line 3 is represented by a quite complicated expression tree (I'm not going to show it here) where the various projected properties live somewhere deep in the tree. So, we'll need to traverse the "projection expression tree" to find all of those properties that we need to load. This way, we'll make our implementation more efficient than if we would just load all of the properties based on the original entity type class (which would be equivalent to a SELECT * FROM ... query in SQL while you only need the value of one single column for instance).

Notice that the System.DirectoryServices.DirectorySearcher class has quite a lot of interesting features that might prove useful when creating a really good LINQ-to-LDAP implementation:

  • SizeLimit allows to limit the number of results returned; this looks a bit like "SELECT TOP ..." or the Take-method from LINQ.
  • PageSize could help to do paged searchs (a bit like Skip+Take) but it could be used to make queries more efficient too. After all, the query only starts executing when you iterate over the resulting IQuerable<...> object, in which you can use iterators to return results element-per-element. Using paging, you could load say 10 results at a time and while the iterator is going over the current set of 10 results, you could start loading the next 10 results behind the scenes (read: on a background thread). This comes in the neighborhood of PLINQ (although a typical PLINQ-scenario consists of grabbing data from multiple data sources in parallel before feeding the results into a join operation).

To complete this entity discussion, here are the definitions for the custom attributes:

Entity-supporting custom attributes - Copy Code
1 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 2 class DirectorySchemaAttribute : Attribute 3 { 4 public DirectorySchemaAttribute(string schema) 5 { 6 Schema = schema; 7 } 8 9 public string Schema { get; set; } 10 } 11 12 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 13 class DirectoryAttributeAttribute : Attribute 14 { 15 private string attribute; 16 17 public DirectoryAttributeAttribute(string attribute) 18 { 19 Attribute = attribute; 20 } 21 22 public string Attribute { get; set; } 23 }

The creation of custom attributes shouldn't be too much of a problem I guess; if it is, more info can be found on MSDN. We'll extend these custom attributes a little more in future posts since there are still a few boobytraps in the AD jungle that will come and get us :-). For now, we're satisfied with this simple definition though.

 

What's next?

In the next post, we'll start the real tree parsing work inside the CreateQuery method. Or, in other words, we'll start the implementation effort for DirectorySource<T> itself. Stay tuned!

Read on ... The IQueryable tales - LINQ to LDAP - Part 4: Parsing and executing queries

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: ,

Comments

# New "Orcas" Language Feature: Lambda Expressions

Sunday, April 08, 2007 4:22 PM by ScottGu's Blog

Last month I started a series of posts covering some of the new VB and C# language features that are

# New "Orcas" Language Feature: Lambda Expressions

Sunday, April 08, 2007 4:39 PM by BusinessRx Reading List

Last month I started a series of posts covering some of the new VB and C# language features that are

# New "Orcas" Language Feature: Lambda Expressions

Sunday, April 08, 2007 4:40 PM by ASP.NET

Last month I started a series of posts covering some of the new VB and C# language features that are

# re: The IQueryable tales - LINQ to LDAP - Part 3: Why do we need entities?

Monday, April 09, 2007 4:43 PM by SvetMy

I'm very interested in finding a way to implement transactional Active Directory updates, i.e. including AD operations in a transaction? It seems like it's theoretically possible with LINQ?

What's your thoughts on this?

Thanks for the great articles!!!

# The IQueryable tales - LINQ to LDAP - Part 2: Getting started with IQueryable<T>

Monday, April 09, 2007 6:40 PM by B# .NET Blog

Introduction Welcome back to the LINQ-to-LDAP series. So far, we've been discussing: Part 0: Introduction

# 新Orcas语言特性:Lambda表达式

Monday, April 09, 2007 10:39 PM by shoutor

什么是Lambda表达式?Lambda表达式为编写匿名方法提供了更简明的函数式的句法,但结果却在编写LINQ查询表达式时变得极其有用,因为它们提供了一个非常紧凑的而且类安全的方式来编写可以当作参数来传递,在以后作运算的函数。

# Nueva caracter??stica de "Orcas": Expresiones Lambda &laquo; Thinking in .NET

PingBack from http://thinkingindotnet.wordpress.com/2007/04/10/nueva-caracteristica-de-orcas-expresiones-lambda/

# The IQueryable tales - LINQ to LDAP - Part 4: Parsing and executing queries

Tuesday, April 10, 2007 3:21 PM by B# .NET Blog

Introduction Welcome back to the LINQ-to-LDAP series. So far, we've been discussing: Part 0: Introduction

# The IQueryable tales - LINQ to LDAP - Part 5: Supporting updates

Tuesday, April 10, 2007 6:29 PM by B# .NET Blog

Introduction Welcome back to the LINQ-to-LDAP series. So far, we've been discussing: Part 0: Introduction

# LINQ to LDAP - Implementation Details

Wednesday, April 11, 2007 12:01 PM by ((Research + Development) - Sleep) > 24

Today, Sam Gentile noted a series of posts by Bart De Smet describing (in great detail) how LINQ queries

# Coming soon - The return of IQueryable<T> - LINQ to SharePoint

Friday, April 13, 2007 4:09 PM by B# .NET Blog

Earlier this week, "The IQueryable Tales" were published on my blog, with great success. This series

# Building Custom LINQ Enabled Data Providers using IQueryable<T>

Friday, April 20, 2007 6:16 AM by Tom's MSDN Belux Corner

Bart De Smet wrote a hands-on tutorial that explains quite in-depth how one can build a custom data provider

# New "Orcas" Language Feature: Lambda Expressions

Tuesday, May 08, 2007 1:22 AM by Tyrannosaurus Rex

Last month I started a series of posts covering some of the new VB and C# language features that are

# Community Convergence XXVII

Sunday, May 13, 2007 11:13 PM by Charlie Calvert's Community Blog

Welcome to the 27th Community Convergence. I use this column to keep you informed of events in the C#

# 新Orcas语言特性:Lambda表达式。

Sunday, June 24, 2007 9:39 AM by 勤勤同学

随VS 2005发布的C#2.0引进了匿名方法的概念,允许在预期代理(delegate)值的地方用“行内(in-line)”代码块(code blocks)来做替代。

# The IQueryable tales - LINQ to LDAP - Part 5: Supporting updates

Friday, July 27, 2007 5:42 AM by B# .NET Blog

Introduction Welcome back to the LINQ-to-LDAP series. So far, we&#39;ve been discussing: Part 0: Introduction

# LINQ to Active Directory (formerly known as LINQ to LDAP) is here

Sunday, November 25, 2007 11:23 PM by B# .NET Blog

Within a few seconds from now I&#39;ll hit the &quot;Publish This Project&quot; button in CodePlex. Back

# LINQ to Active Directory (formerly known as LINQ to LDAP) is here

Sunday, November 25, 2007 11:24 PM by Elan Hasson's Favorite Blogs

Within a few seconds from now I&#39;ll hit the &quot;Publish This Project&quot; button in CodePlex. Back

# LINQ to Active Directory (formerly known as LINQ to LDAP) is here

Friday, December 21, 2007 7:44 AM by Developer Blogs

Within a few seconds from now I&#39;ll hit the &quot;Publish This Project&quot; button in CodePlex. Back

# .NET3.5?????????,Lambda????????? | Richie's Blog

Monday, January 21, 2008 8:58 PM by .NET3.5?????????,Lambda????????? | Richie's Blog

Pingback from  .NET3.5?????????,Lambda????????? | Richie's Blog

# [转贴].NET3.5新特性,Lambda表达式

Wednesday, February 13, 2008 5:04 AM by 菩提树下的杨过

【原文地址】New

# Baby names search - Search for res

Monday, September 21, 2009 7:58 PM by Baby names search - Search for res

Pingback from  Baby names search - Search for res

# ???Orcas???????????????Lambda????????? | A18??????

Monday, December 28, 2009 4:56 PM by ???Orcas???????????????Lambda????????? | A18??????

Pingback from  ???Orcas???????????????Lambda?????????  | A18??????

# ???Orcas???????????????Lambda????????? - Java??????

Thursday, September 02, 2010 6:44 AM by ???Orcas???????????????Lambda????????? - Java??????

Pingback from  ???Orcas???????????????Lambda????????? - Java??????

# RealTime - Questions: "What do we do when any program in codeblocks is not getting built?"

Pingback from  RealTime - Questions: "What do we do when any program in codeblocks is not getting built?"

# Afternoon Coffee 59 &#8211; DevHawk

Sunday, April 17, 2011 3:58 PM by Afternoon Coffee 59 – DevHawk

Pingback from  Afternoon Coffee 59 &#8211; DevHawk