Hi, I am very happy to say that I have new one bonsai tree :), I know I have them everywhere. This one will grow up on my desk in my new workplace. The reason for having bonsai trees and carrying them is because of my favorite team role, the Plant. I like plants, especially bonsai trees, as you may already know. I want to say also that the acting Plant/Creator role is difficult because I usually have many ideas, and of course, not all ideas are good. So I always need someone who helps me evaluate my ideas early, drop this not use one and prevent me from analyzing it and building a prototype that will probably be a waste of my time. Being a plant is mostly about putting ideas like a plant into the ground and carrying the ideas that can grow. It is another nice aspect of caring for small and beautiful trees. I like to say sometimes: “It is straightforward to cut the bonsai branch and almost impossible to put it back.” So before any change, you need to be sure it is correct, or take responsibility for the wrong decision and live with that because there is no way back, and the same is true in life. And I wonder whether you have got an interesting hobby with similar quotes coming from it. Oh, I almost forget to write about my small big success with KinectCam post that was visited more than 1000 times with 475 downloads of New.KinectCam :).
Now after a bit of my philosophy I want to show you a nice way to create very fast Binder class. As you may already know, in n-tier applications, the user interface and business logic layers are usually loosely connected by binding. And this is very good because the binding is done by .NET reflection, so there is no strong connection between your logic class and the user interface. Unfortunately, the binding typically uses property descriptors. So imagine you have the following business POCO light state only C# class.
namespace Application.BussinessObjects.Entites { using System; [Serializable] public class User { public string ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string EducationDegree { get; set; } public string Age { get; set; } } }
It is typical entity that can be loaded and materialized by for example ORM like Entity Framework or NHibernate or even by homemade the data access layer that use pure ADO.NET and IDataReader fast forward reader implementation of your favorite database provider. So it comes to you, and you can bind all the User class properties to your user interface using the Binder class. It is, and it was implemented in WinForms, WPF, WebForms, and even ASP.NET MVC. So, for example, when you use DataBinder from System.Web.dll assembly of .NET 4.0 in the System.Web.UI namespace has the following part.
private static bool enableCaching; public static bool EnableCaching { get { return DataBinder.enableCaching; } set { DataBinder.enableCaching = value; if (!value) { DataBinder.propertyCache.Clear(); } } } private readonly static ConcurrentDictionary<type , PropertyDescriptorCollection> propertyCache; internal static PropertyDescriptorCollection GetPropertiesFromCache(object container) { if (!DataBinder.EnableCaching || container is ICustomTypeDescriptor) { return TypeDescriptor.GetProperties(container); } else { PropertyDescriptorCollection properties = null; Type type = container.GetType(); if (!DataBinder.propertyCache.TryGetValue(type, out properties)) { properties = TypeDescriptor.GetProperties(type); DataBinder.propertyCache.TryAdd(type, properties); } return properties; } } public static object GetPropertyValue(object container, string propName) { if (container != null) { if (!string.IsNullOrEmpty(propName)) { object value = null; PropertyDescriptor propertyDescriptor = DataBinder.GetPropertiesFromCache(container).Find(propName, true); if (propertyDescriptor == null) { object[] fullName = new object[] { container.GetType().FullName, propName }; throw new HttpException("DataBinder_Prop_Not_Found"); } else { value = propertyDescriptor.GetValue(container); return value; } } else { throw new ArgumentNullException("propName"); } } else { throw new ArgumentNullException("container"); } }
You can see whole implementation of this class by using the Telerik JustDecompile or the Red-Gate Reflector tool. Anyway, it is a small, powerful class. And in this entry, I want to show you how to create a much faster implementation using FastMember library that you can get from NuGet. So I do not want to explain to you all the details of the property descriptors. I want to only write for you that it uses reflection and get it based on the Type class, and I hope you see all details clearly. It is not a lot of code, isn’t? Now I will show you two classes and a small benchmark. The first will be WebDataBinder, with the same behavior built in the .NET 4.5 Binder class, and the second one, FastWebDataBinder, uses the FastMember library.
namespace Application.BussinessObjects.Common { using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Web; // for FastWebDataBinder using FastMember; // for Main method benchamerk tests using Application.BussinessObjects.Entites; using System.Diagnostics; public static class WebDataBinder { private static bool enableCaching; public static bool EnableCaching { get { return WebDataBinder.enableCaching; } set { WebDataBinder.enableCaching = value; if (!value) { WebDataBinder.propertyCache.Clear(); } } } private readonly static ConcurrentDictionary<type , PropertyDescriptorCollection> propertyCache = new ConcurrentDictionary</type><type , PropertyDescriptorCollection>(); internal static PropertyDescriptorCollection GetPropertiesFromCache(object container) { if (!WebDataBinder.EnableCaching || container is ICustomTypeDescriptor) { return TypeDescriptor.GetProperties(container); } else { PropertyDescriptorCollection properties = null; Type type = container.GetType(); if (!WebDataBinder.propertyCache.TryGetValue(type, out properties)) { properties = TypeDescriptor.GetProperties(type); WebDataBinder.propertyCache.TryAdd(type, properties); } return properties; } } public static object GetPropertyValue(object container, string propName) { if (container != null) { if (!string.IsNullOrEmpty(propName)) { object value = null; PropertyDescriptor propertyDescriptor = WebDataBinder.GetPropertiesFromCache(container).Find(propName, true); if (propertyDescriptor == null) { object[] fullName = new object[] { container.GetType().FullName, propName }; throw new HttpException("DataBinder_Prop_Not_Found"); } else { value = propertyDescriptor.GetValue(container); return value; } } else { throw new ArgumentNullException("propName"); } } else { throw new ArgumentNullException("container"); } } } public static class FastWebDataBinder { private static bool enableCaching; public static bool EnableCaching { get { return FastWebDataBinder.enableCaching; } set { FastWebDataBinder.enableCaching = value; if (!value) { FastWebDataBinder.propertyCache.Clear(); } } } private readonly static ConcurrentDictionary</type><type , TypeAccessor> propertyCache = new ConcurrentDictionary</type><type , TypeAccessor>(); internal static TypeAccessor GetPropertiesFromCache(object container) { if (!FastWebDataBinder.EnableCaching || container is ICustomTypeDescriptor) { return TypeAccessor.Create(container.GetType()); } else { TypeAccessor typeAccessor; Type type = container.GetType(); if (!FastWebDataBinder.propertyCache.TryGetValue(type, out typeAccessor)) { typeAccessor = TypeAccessor.Create(type); FastWebDataBinder.propertyCache.TryAdd(type, typeAccessor); } return typeAccessor; } } public static object GetPropertyValue(object container, string propName) { if (container != null) { if (!string.IsNullOrEmpty(propName)) { object value = null; TypeAccessor typeAccessor = FastWebDataBinder.GetPropertiesFromCache(container); if (typeAccessor == null) { object[] fullName = new object[] { container.GetType().FullName, propName }; throw new HttpException("DataBinder_Prop_Not_Found"); } else { value = typeAccessor[container, propName]; return value; } } else { throw new ArgumentNullException("propName"); } } else { throw new ArgumentNullException("container"); } } } class Program { static void Main(string[] args) { var data = new List<user>(); for (int p = 0; p < 1000000; ++p) { data.Add(new User { ID = p.ToString(), FirstName = "Piotr" + p, LastName = "Sowa" + p, EducationDegree = "MEng", Age = ((p + 5) % 75).ToString() }); } for (int i = 0; i < 10; ++i) { var meter = Stopwatch.StartNew(); for (int p = 0; p < 1000000; ++p) { var container = data[p]; var id = WebDataBinder .GetPropertyValue(container, "ID"); var firstName = WebDataBinder .GetPropertyValue(container, "FirstName"); var lastName = WebDataBinder .GetPropertyValue(container, "LastName"); var educationDegree = WebDataBinder .GetPropertyValue(container, "EducationDegree"); var age = WebDataBinder .GetPropertyValue(container, "Age"); var expectedId = p.ToString(); var expectedFirstName = "Piotr" + p; var expectedLastName = "Sowa" + p; var expectedEducationDegree = "MEng"; var expectedAge = ((p + 5) % 75).ToString(); var correct = expectedId.Equals(id) && expectedAge.Equals(age) && expectedFirstName.Equals(firstName) && expectedLastName.Equals(lastName) && expectedEducationDegree.Equals(educationDegree); if (!correct) { Console.WriteLine("Error"); } } meter.Stop(); Console.WriteLine( "PropertyDescriptor: 5 millions reads of properties took {0}ms.", meter.ElapsedMilliseconds); var meterFast = Stopwatch.StartNew(); for (int p = 0; p < 1000000; ++p) { var container = data[p]; var id = FastWebDataBinder .GetPropertyValue(container, "ID"); var firstName = FastWebDataBinder .GetPropertyValue(container, "FirstName"); var lastName = FastWebDataBinder .GetPropertyValue(container, "LastName"); var educationDegree = FastWebDataBinder .GetPropertyValue(container, "EducationDegree"); var age = FastWebDataBinder .GetPropertyValue(container, "Age"); var expectedId = p.ToString(); var expectedFirstName = "Piotr" + p; var expectedLastName = "Sowa" + p; var expectedEducationDegree = "MEng"; var expectedAge = ((p + 5) % 75).ToString(); var correct = expectedId.Equals(id) && expectedAge.Equals(age) && expectedFirstName.Equals(firstName) && expectedLastName.Equals(lastName) && expectedEducationDegree.Equals(educationDegree); if (!correct) { Console.WriteLine("Error"); } } meterFast.Stop(); Console.WriteLine( "FastMember: 5 millions reads of properties took {0}ms.", meterFast.ElapsedMilliseconds); } Console.WriteLine("Press any key to close..."); Console.ReadKey(true); } } }
And here is an output:
PropertyDescriptor: 5 millions reads of properties took 14812ms. FastMember: 5 millions reads of properties took 1025ms. PropertyDescriptor: 5 millions reads of properties took 14599ms. FastMember: 5 millions reads of properties took 1027ms. PropertyDescriptor: 5 millions reads of properties took 14595ms. FastMember: 5 millions reads of properties took 993ms. PropertyDescriptor: 5 millions reads of properties took 14624ms. FastMember: 5 millions reads of properties took 991ms. PropertyDescriptor: 5 millions reads of properties took 14567ms. FastMember: 5 millions reads of properties took 1014ms. PropertyDescriptor: 5 millions reads of properties took 14632ms. FastMember: 5 millions reads of properties took 1010ms. PropertyDescriptor: 5 millions reads of properties took 14618ms. FastMember: 5 millions reads of properties took 1005ms. PropertyDescriptor: 5 millions reads of properties took 14612ms. FastMember: 5 millions reads of properties took 987ms. PropertyDescriptor: 5 millions reads of properties took 14593ms. FastMember: 5 millions reads of properties took 1006ms. PropertyDescriptor: 5 millions reads of properties took 14605ms. FastMember: 5 millions reads of properties took 1000ms. Press any key to close...
And another one when I comment out checking if data is correct. Equals function is not very fast also you know.
PropertyDescriptor: 5 millions reads of properties took 14111ms. FastMember: 5 millions reads of properties took 469ms. PropertyDescriptor: 5 millions reads of properties took 13826ms. FastMember: 5 millions reads of properties took 463ms. PropertyDescriptor: 5 millions reads of properties took 13824ms. FastMember: 5 millions reads of properties took 464ms. PropertyDescriptor: 5 millions reads of properties took 13837ms. FastMember: 5 millions reads of properties took 465ms. PropertyDescriptor: 5 millions reads of properties took 13844ms. FastMember: 5 millions reads of properties took 453ms. PropertyDescriptor: 5 millions reads of properties took 13865ms. FastMember: 5 millions reads of properties took 456ms. PropertyDescriptor: 5 millions reads of properties took 13917ms. FastMember: 5 millions reads of properties took 446ms. PropertyDescriptor: 5 millions reads of properties took 13815ms. FastMember: 5 millions reads of properties took 460ms. PropertyDescriptor: 5 millions reads of properties took 13838ms. FastMember: 5 millions reads of properties took 463ms. PropertyDescriptor: 5 millions reads of properties took 13806ms. FastMember: 5 millions reads of properties took 453ms. Press any key to close...
And another one when I enabled caching.
PropertyDescriptor: 5 millions reads of properties took 2806ms. FastMember: 5 millions reads of properties took 465ms. PropertyDescriptor: 5 millions reads of properties took 2778ms. FastMember: 5 millions reads of properties took 451ms. PropertyDescriptor: 5 millions reads of properties took 2806ms. FastMember: 5 millions reads of properties took 434ms. PropertyDescriptor: 5 millions reads of properties took 2848ms. FastMember: 5 millions reads of properties took 445ms. PropertyDescriptor: 5 millions reads of properties took 2779ms. FastMember: 5 millions reads of properties took 443ms. PropertyDescriptor: 5 millions reads of properties took 2770ms. FastMember: 5 millions reads of properties took 438ms. PropertyDescriptor: 5 millions reads of properties took 2769ms. FastMember: 5 millions reads of properties took 439ms. PropertyDescriptor: 5 millions reads of properties took 2769ms. FastMember: 5 millions reads of properties took 436ms. PropertyDescriptor: 5 millions reads of properties took 2806ms. FastMember: 5 millions reads of properties took 434ms. PropertyDescriptor: 5 millions reads of properties took 2802ms. FastMember: 5 millions reads of properties took 439ms. Press any key to close...
So FastMember library based Binder is almost 30 times faster than classic property descriptor based one:). I know enabled caching helps with property descriptor, but usually we do not cache property descriptors for types we want to bind because there are hundreds of them. There is one more thing. In .NET 4.5, reflection was tuned up about 4 times, so imagine that you are using .NET 4.0-based solution, which should increase performance with the new binder about 120 times :). Enjoy!
P ;).