Monday, June 11, 2012

LINQ to LDAP: Documentation

Just a minor update. 3.0 is coming. I'm currently reviewing and updating the documentation over at CodePlex. I realized that I haven't touched it since sometime around 1.5 / 2.0 so it's in dire need of a refresh. There are a few things from 3.0 in there so don't download 2.5 and scream "WTF, this is all wrong!" :).

Saturday, June 2, 2012

Stop Checking CanExecute!

I stumbled upon an interesting problem in WPF. I've only seen a few rumblings about it on the internet so it may just be me doing something stupid. I can reproduce it so that's enough for me to look into it.

It's a common setup. There's a view model that manages workspaces (tabs, windows, whatever) similar to Josh Smith's example. However, when removing a view model from the ObservableCollection my view continues to poll the CanExecute of the commands it's bound too. No big deal, just set them to null and raise property changed and problem solved. Well what happens if you just want to unbind your current view and rebind to a new view? The view model has all the state in MVVM after all.

You could have some sort of unbind implementation on your workspaces that will null your commands and reinitialize them (bleh). The simplest option I've found is to set DataContext to null on your view when it unloads.


You can then just inherit from this control and call it a day.


Here's a project demonstrating the problem.

Saturday, March 17, 2012

LINQ To LDAP: 2.5

Been a while since my last post. There are quite a few changes in 2.5, but I want to focus on the biggest ones.

Multiple Naming Contexts and Configurations
First big change is now you can query for mapped objects in any naming context. This makes it easier to reuse a mapping for different parts of the directory or even with a completely different directory. The naming context you specify here will always override whatever you have mapped.
context.Query<User>(namingContext: "naming context");

This leads me to the next change. I've had a lot of questions about having dynamic configurations and multiple directory support. I decided to give you more control over the lifespan of LdapConfiguration. I've also exposed DirectoryMapper so you can share your mappings among multiple configurations. Each configuration has it's own connection factory so this should handle connecting to multiple directories.

You can store your configurations with your favorite IoC container, in a static class, or whatever your preference. You can still use a single global one by calling UseStaticStorage on the configuration.

Custom Mapping
I've also added support for mapping properties to custom values. There's a new method on ClassMap called MapCustom. Here's an example of mapping ObjectGuid as a custom property:
MapCustom(s => s.ObjectGuid).Named("objectGuid")
    .ConvertFromDirectoryUsing(d =>
                                    {
                                        var bytes = d.GetValues(typeof(byte[]))[0] as byte[];
                                        return new Guid(bytes);
                                    })
    .ConvertToFilterUsing(v => v.ToStringOctet())
    .ConvertToDirectoryUsing(v => v.ToByteArray())
    .CompareChangesUsing((guid1, guid2) => guid1.Equals(guid2));
At a minimum you have to implement ConvertFromDirectoryUsing. If you want to filter by it you have to implement ConvertToFilterUsing. And if you want to update the property then you have to implement ConvertToDirectoryUsing. CompareChangesUsing is optional. If you've implemented IDrectoryObject or subclassed DirectoryObjectBase and your custom mapping is for something that can't really use the default Equals implementation then you'll want to implement it.

I've also added support for forwarding values in place of others. Right now it's only available when mapping DateTimes (due to the 9223372036854775807 value meaning "not set"). I plan to expand this to other property types if necessary. Here's how you use it:
Map(c => c.AccountExpires)
    .Named("accountExpires")
    .DateTimeFormat(null)
    .DirectoryValue("9223372036854775807").Returns(null)
    .InstanceValue(DateTime.MinValue).Sends("9223372036854775807")
    .InstanceValueNullOrDefault().Sends("9223372036854775807");

Querying
I just want to add a small note about the Filter class. Previously I was escaping any special characters you passed in. I think now that I should give you more control over the values. You can now use Filter.Equal("attribute", "*"). However, if you want to escape any special characters you can use Filter.Equal("attribute", "some value".CleanFilterValue()).

You can read more about what's in this release here.

Wednesday, January 11, 2012

LINQ To LDAP: More CRUD

I just realized that I never actually finished writing about CRUD operations with LINQ to LDAP. I guess I don't have to worry too much because someone else did it for me! He plans to have a full series on using LINQ to LDAP with Visual Studio LightSwitch.

Tuesday, December 27, 2011

LINQ To LDAP: Visiting the Locals

I recently ran into a problem with how LINQ to LDAP's expression visitor was working. This expression:
source.Where(u=> objectGuid.HasValue && u.objectGuid == objectGuid.Value)
was throwing a NullReferenceException. The problem was related to that first condition. The visit constant for QueryTranslator was making an assumption that for each constant it found there would be a corresponding member access.

My hackish solution was simply to ignore constants that don't have a corresponding property. Well that worked, but I saw two problems. First it created unnecessarily complex filters (& and | where one wasn't necessary). The second issue was the InvalidOperationException caused when .Value was called for a nullable without a value.

So fast forward several days of on and off coding (Christmas is for family and coding!) and I think I have a solution. I already borrowed the SubtreeEvaluator so I just needed to create a visitor that would rewrite only boolean constants. Well, I ended up creating two. One to rewrite and then another to remove redundant and unnecessary values. When I finish refactoring, you'll be able to find the BooleanRewriter and BooleanReducer in the Visitors folder.
//if objectGuid doesn't have a value the expression will reduce to:
source.Where(u=> false)

//if objectGuid has a value then the expression will reduce to:
source.Where(u=> u.objectGuid == objectGuid.Value)

Tuesday, December 13, 2011

SharePoint as a document database

No, not the No-SQL kind. I'm nearing completion of a project at work where our internal customer wanted a central repository for their documents. The catch? SharePoint tends to be a bad word among most users. Thankfully SharePoint 2010 ships with a nice RESTful service exposed via ListData.svc so we can leverage SharePoint without anyone really knowing about it.

Following Corey Roth's blog post and this MSDN article, I was able to easily query for documents using LINQ. You even get an index back, but I had to parse it from the server response when adding the document since it gives you the full location as "http://<sharepoint-server>/<site>/_vti_bin/ListData.svc/<ListName>(<Index>)". A lot of this is off the top of my head (sorry, can't really post code samples from my work code):
var service = new MyListDataService(new Uri("http://<sharepoint-server>/<site>/_vti_bin/ListData.svc"))
{
     Credentials = CredentialCache.DefaultCredentials
};

string path = "/<site>/<ListName>/doc.txt";
string contentType = "plain/text";

DocumentItem item = new DocumentItem()
{
    ContentType = contentType,
    Name = "doc.txt",
    Path = path,
    Title = "doc"
};
            
service.AddToDocuments(item);

byte[] data = //wherever the raw file data comes from

service.SetSaveStream(item, data, false, contentType, path);

var response = service.SaveChanges();

int id = //code to parse the index from the Location header of the response

var document = service.Documents.FirstOrDefault(d => d.Id == id);

So I can save and find the document, but how do I edit it? That was a little trickier. My initial idea was just to use Process.Start on the url for the document (this is a WPF app by the way), but for some reason SharePoint defaults to read only when you do that. Nick Grattan has a great solution that I modified to use dynamics since it works so well when you have to bust out some COM code.
var docUrl = "http://<sharepoint-server>/<site>/<ListName>/doc.txt";
Type t = Type.GetTypeFromProgID("SharePoint.OpenDocuments.3");
dynamic sharePointControl = Activator.CreateInstance(t);

sharePointControl.EditDocument(docUrl, string.Empty);

And voilĂ ! Now I can store meta data about the document in my app database, retrieve it from SharePoint and get the power of locking and versioning! I wish I could take credit for the idea, but my boss recommended it. I just thought I'd share the adventure of trying to make it work.

Sunday, December 4, 2011

LINQ To LDAP: Configuration

There have been more than a few questions about the use of static properties within LdapConfiguration. This was intended for simplicity, but it's always nice to have an option for more complex applications.

With the recent changes you now can control the lifetime of your configuration as well as have multiple configurations within a project. Configurations are self contained with their own DirectoryMapper and ConnectionFactory. You can still choose to have a global configuration by calling UseStaticStorage on LdapConfiguration.
var config = new LdapConfiguration()
    .AddMapping(new AttributeClassMap<User>())
    .MaxPageSizeIs(500)
    .LogTo(new SimpleTextLogger(Console.Out))
    .UseStaticStorage();

config.ConfigurePooledFactory("localhost")
    .MaxPoolSizeIs(50)
    .MinPoolSizeIs(0)
    .ProtocolVersion(3);

//will look for the static Configuration property on LdapConfiguration
using (var context = new DirectoryContext())
{
}

//the next two statements construct a DirectoryContext in the same manner
using (var context = config.CreateContext())
{
}

using (var context = new DirectoryContext(config))
{
}

//will create a LdapConfiguration local to this instance.  
//As a result, no mappings can be cached beyond the lifetime 
//for this DirectoryContext.
using (var context = new DirectoryContext(new LdapConnection("localhost"), true))
{
}