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
- 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 Code1 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 Code1 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 Code1 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:
- In Visual Studio "Orcas", a LINQ to SQL file is made:
- Next, tables and/or sprocs are dragged-and-dropped from the Server Explorer to the Object Relational Designer surface:
- Finally, queries can be written like this:
Copy Code1 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 Code1 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 Code1 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 Code1 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: C# 3.0, LINQ