In our last installment, we learned the basics of how to use DictionaryAdapter to use an interface to access loosely structured data. This time, we’re going to show a somewhat more complex scenario than simple properties. Consider the following:
static void Main(string[] args) { var dict = new Dictionary<string, object>() { { "UserId", 1234567 }, { "UserName", new Dictionary<string,object>() { { "UserFirstName", "Tim" }, { "UserLastName", "Rayburn" } } } }; }
As you can see, we’ve nested dictionaries this time, where the UserName key has a value of another dictionary, with two keys itself. The simplest model for this is to simple add another interface, IUserName, which represents this nested data. Here is a look at that code:
public interface IUserData { [Key("UserId")] int Id { get; set; } [Key("UserName")] IUserName Name { get; set; } } public interface IUserName { [Key("UserFirstName")] string FirstName { get; set; } [Key("UserMiddleName")] string MiddleName { get; set; } [Key("UserLastName")] string LastName { get; set; } }
So lets see how DictionaryAdapter (it’s called DA by its friends, which if you’re still reading at this point you clearly are) so lets see how DA handles this scenario. We’d hope that something like this would work:
static void Main(string[] args) { var dict = new Dictionary<string, object>() { { "UserId", 1234567 }, { "UserName", new Dictionary<string,object>() { { "UserFirstName", "Tim" }, { "UserLastName", "Rayburn" } } } }; var data = new DictionaryAdapterFactory().GetAdapter<IUserData>(dict); Console.WriteLine(data.Name.FirstName); }
Does it? Nope. Why? Let’s look at the details:
System.InvalidCastException was unhandled Message=Unable to cast object of type 'System.Collections.Generic.Dictionary`2[System.String,System.Object]' to type 'Part2.IUserName'. Source=Part2.Part2.IUserData.DictionaryAdapter StackTrace: at Part2.UserDataDictionaryAdapter.get_Name() at Part2.Program.Main(String[] args) in C:\source\BlogPosts\DictionaryAdapterIsLove\Part2\Program.cs:line 32 at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args) at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args) at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() at System.Threading.ThreadHelper.ThreadStart_Context(Object state) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.ThreadHelper.ThreadStart() InnerException:
I have to give credit where credit is due, this make pretty darned clear what the problem is. Our nested dictionary is not, itself, being wrapped so when DA tries to retrieve it and cast it to IUserName it, obviously, cannot.
That’s great Tim but how do I fix that?
Enter behaviors. Dictionary adapter has an abstraction over anything that modifies how it does its work called Behaviors. You can create behaviors for all sorts of things, but one of them is Property Getting. To modify this behavior you need to do two things:
- Create a class which implements the IDictionaryPropertyGetter interface.
- Modify your request to the Factory to request your DA use that behavior.
If we take a peek at IDictionaryPropertyGetter you will see the following definition:
public interface IDictionaryPropertyGetter : IDictionaryBehavior { object GetPropertyValue(IDictionaryAdapter dictionaryAdapter, string key, object storedValue, PropertyDescriptor property, bool ifExists); }
As you can see this isn’t terribly complex. But you’ll also notice that it implements IDictionaryBehavior, so lets take a peek at that shall we?
public interface IDictionaryBehavior { int ExecutionOrder { get; } }
Again, not very complex at all. So we need a class that will implement both of these, and which will examine the propertyDescriptor parameter to determine if the requested Type is itself an interface which is not assignable from the type of storedValue but that storedValue is assignable from IDictionary. If those conditions are met, we can create another DictionaryAdapter to adapt to the new interface. Here is a simple example of that class :
public class NestedDictionaryGetterBehavior : IDictionaryPropertyGetter { public object GetPropertyValue(IDictionaryAdapter dictionaryAdapter, string key, object storedValue, PropertyDescriptor property, bool ifExists) { if (property.PropertyType.IsAssignableFrom(storedValue.GetType())) { return storedValue; } if (property.PropertyType.IsInterface && IsDictionary(storedValue.GetType())) { return dictionaryAdapter.This.Factory.GetAdapter(property.PropertyType, storedValue as IDictionary); } return storedValue; } public int ExecutionOrder { get { return 0; } } private bool IsDictionary(Type type) { return typeof(IDictionary).IsAssignableFrom(type); } }
As you can see, some code, but not lots of code. Our example should skip the first if statement and go into the second one, returning the new DictionaryAdapter. Now we need to do step 2, modify our original request to the Factory to add our behavior. Here is what the code looks like once we’ve done that:
static void Main(string[] args) { var dict = new Dictionary<string, object>() { { "UserId", 1234567 }, { "UserName", new Dictionary<string,object>() { { "UserFirstName", "Tim" }, { "UserLastName", "Rayburn" } } } }; var data = new DictionaryAdapterFactory() .GetAdapter(typeof(IUserData), dict, new DictionaryDescriptor().AddBehavior(new NestedDictionaryGetterBehavior())) as IUserData; Console.WriteLine(data.Name.FirstName); Console.ReadLine(); }
Now, that gets a little tedious to do every time, and I can assure you that Craig Neuwirt hates tedious. As such, DictionaryAdapter will look for any attributes on your interface which implement IDictionaryBehavior and add them for you automatically. So with a very simple refactoring of our class:
public class NestedDictionaryGetterBehavior : Attribute, IDictionaryPropertyGetter
We can then modify our interface:
[NestedDictionaryGetterBehavior] public interface IUserData { [Key("UserId")] int Id { get; set; } [Key("UserName")] IUserName Name { get; set; } }
And move back to a very simple DictionaryAdapterFactory request:
var data = new DictionaryAdapterFactory().GetAdapter<IUserData>(dict);
And I think that’s enough for this time.