Monday, October 31, 2005

Speeding access to properties by caching accessors.

When writing generic frameworks, such as O/R Mappers or UI Mapping Frameworks, you inevitably run into the need to access the members (fields or properties) of another class to bind data to the database call parameters or user interface controls. This process is essentially trivial in .Net and implementation exampled are everywhere. All of them rely on some use of the Reflection classes in .Net. I've heard too many complaints about the speed of systems leveled against the use of Reflection, so I spent a little effort getting things zippy on the two frameworks I use on a daily basis. I'm a huge proponent of not doing premature optimization, but when coding framework-level classes, it is good idea to practice good design principles and make the things that stand out during timing runs as quick as possible (while still being maintainable). As a sidebar, the frameworks I am using for O/R Mapping and generic UI are Paul Wilson's excellent ORMapper and UIMapper. ORMapper is very mature, and does most of what I need in an O/R Mapper. UI Mapper is still a 1.00 release, and I've got tons of changes for Paul once he gets time to play with it again, but he's been very open to changes in the past. I can stress how good a deal this software is, everything that Paul's got on the site in good C# code for $50! Anywho, the short of this is that I've got a MemberAccessor cache class that makes sure the reflection is done once, and all the MemberInfo and/or FieldInfo is cached so everything runs very quickly each subsequent use. This stuff works in service classes, WinForms and ASP.Net so don't worry about application or dependancies on things like HttpContext.Cache. Still to do is adding a Lightweight Code Generation to emit the calling stubs, which is a facinating new feature of .Net 2.0 based on the DynamicMethod class. Much of this is based on the ideas give in Joel Pobar's blog entry and his article on MSDN Here's the class, hit me up with any questions:

using System;
using System.Collections.Generic;
using System.Reflection;

namespace Phydeaux.Mapping.Utility
{
    public class MemberAccessor
    {
        internal RuntimeMethodHandle Get;
        internal RuntimeMethodHandle Set;
        internal RuntimeFieldHandle Field;

        internal bool HasGet
        {
            get { return (this.Get.Value != IntPtr.Zero); }
        }

        internal bool HasSet
        {
            get { return (this.Set.Value != IntPtr.Zero); }
        }

        internal bool HasField
        {
            get { return (this.Field.Value != IntPtr.Zero); }
        }

        internal bool Settable
        {
            get { return this.HasSet || this.HasField; }
        }

        internal bool Gettable
        {
            get { return this.HasGet || this.HasField; }
        }

        internal bool AnyDefined
        {
            get { return this.HasGet || this.HasSet || this.HasField; }
        }

        internal bool FullyDefined
        {
            get { return this.HasGet && this.HasSet && this.HasField; }
        }
    }

    public class MemberCacheKey : IEquatable
    {
        internal RuntimeTypeHandle TypeHandle;
        internal string Member;

        internal MemberCacheKey(Type type, string member)
        {
            this.TypeHandle = type.TypeHandle;
            this.Member = member;
        }

        public override bool Equals(object other)
        {
            // covers both null and same reference check...
            if (System.Object.ReferenceEquals(this, other))
                return true;

            return this.Equals(other as MemberCacheKey);
        }

        public override int GetHashCode()
        {
            return (TypeHandle.Value.GetHashCode() << 5) ^ Member.GetHashCode();
        }

        #region IEquatable Members
        public bool Equals(MemberCacheKey other)
        {
            // covers both null and same reference check...
            if (System.Object.ReferenceEquals(this, other))
                return true;

            if (other == null)
                return false;

            return TypeHandle.Equals(other.TypeHandle)
                && Member.Equals(other.Member);
        }
        #endregion

        public class Comparer :  IEqualityComparer
        {
            #region IEqualityComparer Members
            public bool Equals(MemberCacheKey x, MemberCacheKey y)
            {
                // covers both null and same reference check...
                if (System.Object.ReferenceEquals(x, y))
                    return true;

                if (x == null)
                    return false;

                return x.Equals(y);
            }

            public int GetHashCode(MemberCacheKey obj)
            {
                if (obj == null)
                    return 0;

                return obj.GetHashCode();
            }
            #endregion
        }
    }

    public class AccessorCache : Dictionary
    {
        const MemberTypes WhatMembers = MemberTypes.Field | MemberTypes.Property;
        const BindingFlags WhatBindings = BindingFlags.SetProperty | BindingFlags.SetField 
                                            | BindingFlags.GetProperty | BindingFlags.GetField 
                                            | BindingFlags.Public | BindingFlags.NonPublic
                                            | BindingFlags.Instance | BindingFlags.DeclaredOnly;

        public AccessorCache()
            : base(new MemberCacheKey.Comparer())
        {
        }

        public MemberAccessor GetAccessor(Type entityType, string member)
        {
            MemberCacheKey key = new MemberCacheKey(entityType, member);
            MemberAccessor accessor;

            if (!this.TryGetValue(key, out accessor))
            {
                if (BuildAccessor(entityType, member, out accessor))
                {
                    this.Add(key, accessor);
                }
                else
                {
                    throw ArgumentValidation.Decorate(
                        new UIMapperException("cannot build accessor")
                        , MethodBase.GetCurrentMethod(), entityType, member);
                }
            }

            return accessor;
        }

        private bool BuildAccessor(Type entityType, string member, out MemberAccessor accessor)
        {
            accessor = new MemberAccessor();
            return BuildAccessorRecursive(entityType, member, accessor);
        }

        private bool BuildAccessorRecursive(Type entityType, string member, MemberAccessor accessor)
        {
/// TODO build a LCG delegate like http://msdn.microsoft.com/msdnmag/issues/05/07/Reflection/default.aspx
            if (entityType == null || entityType == typeof(Object))
                return accessor.AnyDefined;

            MemberInfo[] members = entityType.GetMember(member, WhatMembers, WhatBindings);

            // look for a property
            foreach (MemberInfo someMember in members)
            {
                if (someMember.MemberType == MemberTypes.Property)
                {
                    PropertyInfo property = (PropertyInfo) someMember;

                    if (property.CanRead && ! accessor.HasGet)
                    {
                        accessor.Get = property.GetGetMethod(true).MethodHandle;
                    }

                    if (property.CanWrite && !accessor.HasSet)
                    {
                        accessor.Set = property.GetSetMethod(true).MethodHandle;
                    }
                }

                if (someMember.MemberType == MemberTypes.Field && !accessor.HasField)
                {
                    FieldInfo field = ((FieldInfo) someMember);
                    accessor.Field = field.FieldHandle;
                }
            }

            return accessor.FullyDefined
                || BuildAccessorRecursive(entityType.BaseType, member, accessor);
        }
    }        
}

Wednesday, October 26, 2005

Update to the integration between Team Foundation Server Version Control and Beyond Compare

Looks like I was overly pessimistic about Microsoft. James Manning at Microsoft (a Team Foundation Version Control guy) dropped in with a few comments to help things along. Adding the /title1=%6 /title2=%7 to the command line arguments in Visual Studio helps the screen display. Thanks James. Craig at Scooter Software has also addressed some of these issues. The just released 2.4 version of the ImageViewer from Scooter Software does understand the image file format without needing the extension adjustment (thanks Craig). I still would love to see Scooter Software update the engine to ignore all the stuff following the ; so we can not have the overly aggressive patterns in the Rules setup in Beyond Compare, or have Microsoft use subdirectories or something instead of the odd file names, but I'm certainly happy for now. UPDATE: James Manning from Microsoft informs me that they've fixed this in the post-beta-3 bits. Thanks, gang! UPDATE #2: The RC has this issue fixed, so you can change the filters back as needed. Also, James has a nice post on configuring for various tools here.

Tuesday, October 25, 2005

Microsoft Team Foundation Server and Beyond Compare

Simply put, I can't stand any other comparison tool than Beyond Compare. This is a product that definitely earns the name!. I would pay more than Abraxis Merge for Beyond Compare, but I don't have to, as they are also the most reasonably priced tool I've ever seen. I use Beyond Compare to do file differences, directory comparisons, heck I use it to upload the latest version of XBox Media Center to my hacked XBox (I use the pimped edition). I'm also using Microsoft Team Foundation Server's source control. Here's how to set Beyond Compare up as the comparison tool: In Visual Studio, click menu File / Options... Expand Source Control in the treeview. Click Visual Studio Team Foundation Server in treeview. Click button Configure User Tools... Click button Add... Enter .* in the Extension textbox Choose Compare in Operation combobox Click button ... next to Command textbox Browse to BC2.EXE in file chooser and click button Open Enter %1 %2 /title1=%6 /title2=%7 into the Arguments textbox. Ok on out. Microsoft's version of these instructions is here. UPDATE: James Manning has comprehensive instructions for other tools here. Now one issue remains, and hopefully either Microsoft or Scooter Software will do something about it. When using an external comparison tool Visual Studio checks both files out to a temporary directory. It appends a tag to indicate the file version. The suffix looks something like ;C19 where the C indicates this is a changeset, and the 19 is the changeset number. It's also possible to see an ;Lxxx which indicates a label xxx. For more information on the suffixes, see this at the Versionspecs section. UPDATE #2 and #3: Both Microsoft and Scooter have addressed this, the RC of VSTS now preserves the file extension; using the suggestions below will improve the display in BeyondCompare until then. This means the external diff tool command line is something like "BC2.EXE Foo.cs;C19 Foo.cs;C22". In Beyond Compare, the funky changeset tag interferes with the file-extension sniffing in Beyond Compare. The actual quick "files are different" compare thinks these are "any other file", so no rules are run. Secondly, when the viewer for different files is run, it also thinks these are "any other file", so no language-specific or file-extension specific plug-ins run. What is needed is to have Beyond Compare have the comparison and the viewer strip off everything after the last ";" from each filename before determining which rules to run and which viewer to launch. The other option is for Microsoft to not create these oddly named files and instead create a subdirectory for each file that reflects the version/changeset/label, etc. Personally, while I think that the filenames are odd, I'm hoping for a fix from Scooter Software. Why?

  1. Having the suffix on the filenames clearly indicates which one is which in the view header.
  2. Scooter Software moves like an ice-cube on a hot griddle.
  3. Microsoft, especially this close to release, moves like a glacier.
Edit 2005 Oct 25th 11:44CDT: Changed the command line arguments above and posted this note. UPDATE: James Manning from Microsoft has informed me that they've fixed the file name issue in the post-beta 3 bits. Thanks, gang. UPDATE #3: Confirmed fix in the RC

Monday, October 17, 2005

Validation and exceptions.

I was reading a blog entry (that I can't seem to find) here about exception strategies with regard to web service calls. I'll repeat the most important thing first: Only throw exceptions under exceptional circumstances! Do NOT use exceptions for flow-control". Okay, on to the real question, how to handle validation logic. What I usually do for business objects is to have a ArrayList Validate(bool throwError) method that accepts a boolean flag to determine if exceptions should be thrown (hold on there, Nelly! I'll explain) and returns a list of business rule violations. What I do is have client or service code call Validate passing false for the throwError parameter. In this case, the Validate method returns an ArrayList of enumerated values of business rules. Since they are enumerated values, you can easily switch on the enumeration returned and act accordingly. Additionally the value can very easily be localized by using a resource lookup to derive the error message given to the user while still logging the exact error. Now, if someone calls through to the Save method even in the presence of violations (or because they didn't call Validate), then I want to throw an exception, so I internally call Validate inside the Save method and pass true for the throwError parameter. When that error is thrown, the Exception.Data is first filled with all the enumerated business rule violations. This strategy insures that validation messages are available when desired via the Validate method, and that attempts to save bad data are stopped via an exception no matter what business object is being saved and no matter what the call depth. Lastly, the reuse of the Validate method and using an enumeration to encode the business rules allows code to handle problems and error messages to easily be localized. If anyone is interested in some code examples, tag up this post. Here's a quick sample:

enum BusinessRules
{
   UserNameRequired
   , UserNameMinimumLength
}

class BusinessObject
{
   public string UserName;

   public ArrayList Validate(bool throwError)
   {
      ArrayList errors = new ArrayList();

      if (this.UserName == null  this.UserName.Length == 0)
      {
         errors.Add(BusinessRules.UserNameRequired);
      }

      if (this.UserName.Length < ex =" new" errors =" businessObject.Validate(false);"> 0)
      {
      StringBuilder statusMessage = new StringBuilder();

      foreach (BusinessRule error in errors)
      {
         if (error == BusinessRules.UserNameMinimumLength)
         {
            // add stuff to make it long enough as an example of handling the error
            businessObject.UserName += " terse little bugger";
         }
         else
         {
            statusMessage.AppendFormat(GetResource(error), this.UserName);
         }
      }

      view.SaveButton.Enabled = false;
   }
   else
   {
      statusMessage = "Looking good!";
      view.SaveButton.Enabled = false;
   }

   UpdateStatus(statusMessage);
}

Tuesday, October 11, 2005

Nullable<>, SQL NULL, and reference null (what do you mean?)

An interesting post on Wesner Moise's blog shows that C# 3.0 and VB.Net 9.0 don't take the same view of Nullable<>. C# takes the idea that Nullable<> is to bridge between value and reference types, while VB takes the idea that Nullable<> is to mirror SQL's NULL. This is a huge difference and it will be a source of REAL problems, mark my words. Null Comparison

Wednesday, October 05, 2005

Movie Meme - Ajax style

Marc's Musings: Movie Meme I've added my hat to the rack over at twofifty.org, where a little Ajax give you a very responsive UI (if a bit unexplained) lets you check off what movies of the top 250 you've seen. The pop-up search of IMDB is sweet. Did I meantion there's an API? My selections