Tuesday, April 10, 2007 6:29 PM bart

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

 

Introduction

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

In the previous post, we managed to translate a query expression from the LINQ domain to the LDAP domain, allowing retrieval of directory objects in a quite flexible way. One piece is missing however: updating the retrieved entity objects and feeding updates back to Active Directory. In this post, we'll show how this can be made possible.

 

Revised entities

Essentially, when retrieving objects through LINQ-to-LDAP, we can support updates to entities if a few conditions are met:

  1. The objects returned should be the entities themselves; we won't support the case where projections are made. Or, in other words, the query should end with a plain vanilla "select <dummy>" clause.
  2. Entity objects should be "improved" to make them more intelligent, allowing to track changes (i.e. property setter calls).

Luckily, .NET's component model comes to our rescue by providing a concept of "property changes", in the INotifyPropertyChanged interface that's defined as follows:

namespace System.ComponentModel { // Summary: // Notifies clients that a property value has changed. public interface INotifyPropertyChanged { // Summary: // Occurs when a property value changes. event PropertyChangedEventHandler PropertyChanged; } }

Based on this interface, we'll create a so-called DirectoryEntity class that acts as the root class for entities that require update support:

DirectoryEntity class to provide update support - Copy Code
1 public class DirectoryEntity : INotifyPropertyChanged 2 { 3 private DirectoryEntry directoryEntry; 4 5 protected internal DirectoryEntry DirectoryEntry 6 { 7 get { return directoryEntry; } 8 set { directoryEntry = value; } 9 } 10 11 public event PropertyChangedEventHandler PropertyChanged; 12 13 protected void OnPropertyChanged(string propertyName) 14 { 15 if (PropertyChanged != null) 16 PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 17 } 18 }

This class provides an event PropertyChanged (through the INotifyPropertyChanged interface) to notify whenever a property is changed and which will be called through the OnPropertyChanged method. Beside of this, a DirectoryEntry instance is kept, in order to be able feeding back changes to AD.

Next, we'll revise the entities like this:

Entity type for user objects with update support - Copy Code
1 [DirectorySchema("user")] 2 class MyUser : DirectoryEntity 3 { 4 private DateTime expiration; 5 6 [DirectoryAttribute("AccountExpirationDate", DirectoryAttributeType.ActiveDs)] 7 public DateTime AccountExpirationDate 8 { 9 get { return expiration; } 10 set 11 { 12 if (expiration != value) 13 { 14 expiration = value; 15 OnPropertyChanged("AccountExpirationDate"); 16 } 17 } 18 } 19 20 private string first; 21 22 [DirectoryAttribute("givenName")] 23 public string FirstName 24 { 25 get { return first; } 26 set 27 { 28 if (first != value) 29 { 30 first = value; 31 OnPropertyChanged("FirstName"); 32 } 33 } 34 } 35 36 private string last; 37 38 [DirectoryAttribute("sn")] 39 public string LastName 40 { 41 get { return last; } 42 set 43 { 44 if (last != value) 45 { 46 last = value; 47 OnPropertyChanged("LastName"); 48 } 49 } 50 } 51 52 private string office; 53 54 [DirectoryAttribute("physicalDeliveryOfficeName")] 55 public string Office 56 { 57 get { return office; } 58 set 59 { 60 if (office != value) 61 { 62 office = value; 63 OnPropertyChanged("Office"); 64 } 65 } 66 } 67 68 private string accoutName; 69 70 [DirectoryAttribute("sAMAccountName")] 71 public string AccountName 72 { 73 get { return accoutName; } 74 set 75 { 76 if (accoutName != value) 77 { 78 accoutName = value; 79 OnPropertyChanged("AccountName"); 80 } 81 } 82 } 83 84 public bool SetPassword(string password) 85 { 86 return this.DirectoryEntry.Invoke("SetPassword", new object[] { password }) == null; 87 } 88 }

Now, we're out of Automatic Property luck, so we'll write complete property definitions together with a private field. Notice the major change being the presence of OnPropertyChanged calls in the property setters. Also, the class derives from DirectoryEntity and it also contains a method that allows invoking operations on the object through the corresponding DirectoryEntry object, for example to set a password (SetPassword).

To make the creation of these entity properties easier, you could create your own code snippet:

Code snippet for entity properties with change notification support - Copy Code
<?xml version="1.0" encoding="utf-8" ?> <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> <CodeSnippet Format="1.0.0"> <Header> <Title>entityprop</Title> <Shortcut>entityprop</Shortcut> <Description>Code snippet for entity property creation with change notification support</Description> <Author>Bart De Smet</Author> <SnippetTypes> <SnippetType>Expansion</SnippetType> </SnippetTypes> </Header> <Snippet> <Declarations> <Literal> <ID>type</ID> <ToolTip>Property type</ToolTip> <Default>int</Default> </Literal> <Literal> <ID>property</ID> <ToolTip>Property name</ToolTip> <Default>MyProperty</Default> </Literal> <Literal> <ID>field</ID> <ToolTip>The variable backing this property</ToolTip> <Default>myVar</Default> </Literal> </Declarations> <Code Language="csharp"><![CDATA[private $type$ $field$; public $type$ $property$ { get { return $field$;} set { if ($field$ != value) { $field$ = value; OnPropertyChanged("$property$"); } } } $end$]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>

Store it in your %profile%\My Documents\Visual Studio Codename Orcas\Code Snippets\Visual C#\My Code Snippets folder with the name entityprop.snippet:

and you'll be able to write entity properties easily by writing entityprop and pressing TAB twice:

 

Making DirectorySource<T> updatable

Now it's time to make DirectorySource<T> update-aware. We'll start by adding the following method:

Update method - Copy Code
1 private Dictionary<object, HashSet<string>> updates = new Dictionary<object, HashSet<string>>(); 2 3 public void Update() 4 { 5 Type t = typeof(T); 6 DirectorySchemaAttribute[] attr = (DirectorySchemaAttribute[])t.GetCustomAttributes(typeof(DirectorySchemaAttribute), false); 7 8 foreach (var e in updates) 9 { 10 if (e.Key is T && e.Key is DirectoryEntity) 11 { 12 DirectoryEntry entry = ((DirectoryEntity)e.Key).DirectoryEntry; 13 foreach (string property in e.Value) 14 { 15 PropertyInfo i = t.GetProperty(property); 16 17 DirectoryAttributeAttribute[] da = i.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[]; 18 if (da != null && da.Length != 0 && da[0] != null) 19 { 20 if (da[0].Type == DirectoryAttributeType.ActiveDs) 21 { 22 if (attr != null && attr.Length != 0) 23 attr[0].ActiveDsHelperType.GetProperty(da[0].Attribute).SetValue(entry.NativeObject, i.GetValue(e.Key, null), null); 24 else 25 throw new InvalidOperationException("Missing schema mapping attribute for updates through ADSI."); 26 } 27 else 28 entry.Properties[da[0].Attribute].Value = i.GetValue(e.Key, null); 29 } 30 else 31 entry.Properties[i.Name].Value = i.GetValue(e.Key, null); 32 } 33 entry.CommitChanges(); 34 } 35 else 36 throw new InvalidOperationException("Can't apply update because updates type doesn't match original entity type."); 37 } 38 39 updates.Clear(); 40 }

The private member update on line 1 will hold mappings between entity objects (represented as "object") and a list of property names that were updated since results were retrieved. The code for the Update method does the following for each update (line 8 to 37):

  • Retrieves information about the property for the given property name on line 15.
  • Inspects the DirectoryAttributeAttribute custom attribute (if present) to find out which value to set. Ultimately, either the IADs* native object will be touched (line 23) or the LDAP property will be updated directly (line 28 and line 31).
  • Finally, changes are committed on line 33.

This logic will only process DirectoryEntity objects (see line 10).

Note: This centralized approach to updates could be extended with update support on the entities themselves. Our approach is to "batch" together updates, however only on the conceptual level since LDAP only supports "atomic" updates of individual objects (on the DirectoryEntry level in managed code terms). Nevertheless, allowing updates to be made on the entity level itself would be a nice addition (guidelines: register for the update notification event in the DirectoryEntity class itself and provide an "update" collector with a local HashSet to keep the updated fields; finally, add an Update method that is more or less equivalent to the one above).

Next, CreateQuery needs a change to support updates as shown in lines 13-14 below:

Updates to CreateQuery - Copy Code
1 public IQueryable<TElement> CreateQuery<TElement>(Expression expression) 2 { 3 // 4 // Create a new queryable object based on the current one. A copy is needed to guarantee uniqueness across multiple queries. 5 // 6 DirectorySource<TElement> d = new DirectorySource<TElement>(searchRoot, searchScope); 7 d.query = query; 8 d.project = project; 9 d.properties = properties; 10 d.originalType = originalType; 11 d.logger = logger; 12 13 // *** UPDATE *** 14 d.updates = updates; 15 16 // 17 // We expect a method call expression for the chain of LINQ query operator method calls. 18 // 19 MethodCallExpression call = expression as MethodCallExpression;

This change takes care of passing the update dictionary reference. This is important because updates will be applied via the original "directory source" while the update notifications will be added via other instances of the DirectorySource possible (because of the way the CreateQuery method works, i.e. returning new instances of the type all the time). In a similar way, we only want to keep one update dictionary that collects all updates made by possibly more than one query (e.g. you change a user's Office in a first loop over the query results for users in specific offices and next you change the account expiration day for another set of entities that were retrieved with another query from the same directory source).

The next change needs to be applied in GetResults's main loop as shown below:

Updates to GetResults main loop - Copy Code
1 foreach (SearchResult sr in s.FindAll()) 2 { 3 DirectoryEntry e = sr.GetDirectoryEntry(); 4 5 object result = Activator.CreateInstance(project == null ? typeof(T) : originalType); 6 7 /// *** UPDATE *** 8 DirectoryEntity entity = result as DirectoryEntity; 9 if (entity != null) 10 entity.DirectoryEntry = e; 11 12 if (project == null) 13 { 14 foreach (PropertyInfo p in typeof(T).GetProperties()) 15 AssignResultProperty(helper, e, result, p.Name); 16 17 /// *** UPDATE *** 18 if (entity != null) 19 entity.PropertyChanged += new PropertyChangedEventHandler(UpdateNotification); 20 21 yield return (T)result; 22 } 23 else 24 { 25 foreach (string prop in properties) 26 AssignResultProperty(helper, e, result, prop); 27 28 /// *** UPDATE *** 29 if (entity != null) 30 entity.PropertyChanged += new PropertyChangedEventHandler(UpdateNotification); 31 32 yield return (T)project.DynamicInvoke(result); 33 } 34 }

This code first checks whether or not the result object is a DirectoryEntity, thus allows updates to be made. If so, an event handler for PropertyChanged is hooked up in line 19 or line 30. The UpdateNotification method is defined like this:

UpdateNotification event handler - Copy Code
1 void UpdateNotification(object sender, PropertyChangedEventArgs e) 2 { 3 T source = (T)sender; 4 5 if (!updates.ContainsKey(source)) 6 updates.Add(source, new HashSet<string>()); 7 8 updates[source].Add(e.PropertyName); 9 }
This is it!

 

A few samples

Let's show a few queries with updating logic to show the power of this implementation. The first sample shows how to move people to other offices:

Move users to other office
var myusers = new DirectorySource<MyUser>(new DirectoryEntry("LDAP://localhost/OU=Demo,DC=linqdemo,DC=local"), SearchScope.Subtree); // // Query with update functionality using an entity MyUser : DirectoryEntity. // string oldOffice = "Test"; string newOffice = "Demo"; var res7 = from usr in myusers where usr.Office == oldOffice select usr; Console.WriteLine("QUERY 7\n======="); foreach (var u in res7) { Console.WriteLine("{0} {1} works in {2}", u.FirstName, u.LastName, u.Office); u.Office = newOffice; } Console.WriteLine(); Console.WriteLine("Moving people to new office {0}...\n", newOffice); myusers.Update(); int k = 0; foreach (var u in res7) //should be empty now Console.WriteLine("{0} {1} still works in {2}", u.FirstName, u.LastName, u.Office, k++); if (k == 0) Console.WriteLine("No results returned."); //expected case Console.WriteLine();

The result is illustrated below (before, after):

   

Notice that line 25 re-executes the query, which shows the behavior of the lazy execution model. You could implement a slightly other behavior by caching the results obtained in the first iteration over the result set, in order to speed up subsequent iterations. Of course, you'll need support to invalidate the cache in such a case, either by a manual method call or by getting update notifications back from the server in the background (like SQL Server 2005 can do with update notifications and database dependencies).

If you change the query like this:

var res7 = from usr in myusers where usr.Office == oldOffice select new { usr.FirstName, usr.LastName, usr.Office };

update code won't work anymore, since we're not touching the entity objects but instances of "some anonymous type" generated by the compiler when it encountered the new { ... } anonymous type syntax. The code doesn't throw an exception though; you should simply know that this won't update the data source. Therefore, only projections of type "select <dummy>" will yield updatable entity objects.

Another sample is to call SetPassword for every user. The code below does this and checks for validity of the new password to verify the result:

Changing user passwords - Copy Code
1 var res8 = from usr in myusers 2 select usr; 3 4 string newPassword = "Hello W0rld!"; 5 6 Console.WriteLine("QUERY 8\n======="); 7 foreach (var u in res8) 8 { 9 Console.Write("Setting the password of {0} {1}... ", u.FirstName, u.LastName); 10 if (u.SetPassword(newPassword)) 11 { 12 Console.WriteLine("Done."); 13 Console.Write("Validating password... "); 14 try 15 { 16 new DirectoryEntry("LDAP://localhost", u.AccountName, newPassword).RefreshCache(); 17 Console.WriteLine("Successful."); 18 } 19 catch (DirectoryServicesCOMException ex) 20 { 21 Console.ForegroundColor = ConsoleColor.Red; 22 Console.WriteLine(ex.Message); 23 Console.ResetColor(); 24 } 25 } 26 else 27 Console.WriteLine("Failed."); 28 } 29 Console.WriteLine(); 30 31 myusers.Update();

Together with a random password generator you could use this code fragment to reset a set of user passwords. The RefreshCache call on line 16 is a simple trick to make sure that a request is made to the Active Directory domain controller, in order to force an exception to be thrown when the password is invalid. Fortunately, the code runs correctly :-).

Finally, a little sample to show that updating through ADSI (IADs*) properties works fine too:

Expiring accounts - Copy Code
1 var res9 = from usr in myusers 2 select usr; 3 4 Console.WriteLine("QUERY 9\n======="); 5 foreach (var u in res9) 6 u.AccountExpirationDate = DateTime.Now.AddDays(30); 7 Console.WriteLine(); 8 9 myusers.Update();

With the following result:

   

Have fun! You can download the resulting code over here. Notice that this isn't production quality code and is only meant as a sample. Verify all updates before executing the code or you could harm your domain infrastructure.

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

Filed under: ,

Comments

# New "Orcas" Language Feature: Lambda Expressions

Tuesday, April 10, 2007 10:23 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

# LINQ to LDAP - Implementation Details

Wednesday, April 11, 2007 12:02 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

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

Wednesday, April 11, 2007 1:24 PM by SvetMy

Hi Bart!

How about transactional updates?

THanks,

Slava

# 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

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

Friday, April 20, 2007 6:19 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:23 AM by Tyrannosaurus Rex

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

Tuesday, May 08, 2007 1:41 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#

# Community Convergence XXVII

Sunday, May 13, 2007 11:18 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#

# 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'll hit the "Publish This Project" button in CodePlex. Back

# Sesión "LINQ en profundidad” en TechDays 2008

Thursday, February 21, 2008 6:02 PM by Sobre C#, LINQ y algo más...

Un año más, Microsoft España ha tenido a bien confiarme la presentación sobre LINQ en su evento más importante