Tuesday, 05 February 2008

No, this article does not nag about some code I've seen that misuses new features. This is how I did it - on purpose.

I've always disliked the way I usually set up data in the database for testing: recreate the database, create the domain objects, setting all the necessary properties, commit the context. Take this code for example:

DataDomainSchema schema = DataDomainSchema.LoadFrom("SomeMappingFile");
schema.CreateDbSchema(connStr);

DataDomain dd = new DataDomain(schema, connStr);

using (Context.Push(ShortRunningTransactionContext.Create()))
{
  Customer tt = dd.New<Customer>();
  tt.Name = "TechTalk";

  RootProject tt_hk = dd.New<RootProject>();
  tt_hk.Name = "Housekeeping";

  ChildProject tt_hk_hol = dd.New<ChildProject>();
  tt_hk_hol.Name = "Holiday";
  tt_hk.ChildProjects.Add(tt_hk_hol);

  ChildProject tt_hk_ill = dd.New<ChildProject>();
  tt_hk_ill.Name = "Illness";

  tt_hk.ChildProjects.Add(tt_hk_ill);

  tt.RootProjects.Add(tt_hk);

  RootProject tt_g = dd.New<RootProject>();
  tt_g.Name = "Genome";

  ChildProject tt_g_dev = dd.New<ChildProject>();
  tt_g_dev.Name = "Development";
  tt_g.ChildProjects.Add(tt_g_dev);

  ChildProject tt_g_mnt = dd.New<ChildProject>();
  tt_g_mnt.Name = "Maintenance";
  tt_g.ChildProjects.Add(tt_g_mnt);
  tt.RootProjects.Add(tt_g);

  Context.CommitCurrent();
}

What I dislike in this is the 'setting all the necessary properties' part. Part of it is that it's hard to follow the hierarchy of the objects.

The other is that I'm lazy.

Even if I'm typing with considerable speed - and keep pressing ctrl+(alt)+space and let ReSharper do the rest - I still hate it for its repetitiousness. I always wanted to have something like ActiveRecord's Fixtures in Rails - but I never had the time to implement it. Yeah, typical excuse, and that's how we usually lose development time even in the short run, so I know I'll have do it the next time I need to create test data.

Sure, I could always create builder methods for every type to handle, passing in the property values and collections etc, but even creating those is yet another repetitious task. I always longed for some more 'elegant' write-once-use-everywhere kind of framework. So when I read this post, I thought, maybe I can get away with writing a simple, but usable enough, initializer helper extension. Here's the resulting initializing code:

...

using (Context.Push(ShortRunningTransactionContext.Create()))
{
  dd.Init<Customer>().As(
     Name => "TechTalk",
     RootProjects => new Project[] {
       dd.Init<RootProject>().As(
         Name => "Housekeeping", 
         ChildProjects => new Project[] {
           dd.Init<ChildProject>().As(Name => "Holiday"),
           dd.Init<ChildProject>().As(Name => "Illness")
         }),
       dd.Init<RootProject>().As(
         Name => "Genome", 
         ChildProjects => new Project[] {
           dd.Init<ChildProject>().As(Name => "Development"),
           dd.Init<ChildProject>().As(Name => "Maintenance")
         })
       });

  Context.CommitCurrent();
}

Prettier to the eye - but unfortunately, it's still not practical enough. For one thing, it’s easy to represent a tree this way, but it still doesn't offer a solution for many-to-many relations. That's a lesser concern though, and I have ideas for overcoming this (but haven’t done it so far due to lack of time, again). A greater problem is that it's not type safe: the parameter names of the lambdas (Name, RootProjects, ChildProjects) are just that - names, aliases; they are not checked during compile time. Even as a dynamic typed language advocate, I don't like too much dynamic behavior in statically type languages - that usually results in little gain if any, while losing their advantages, even 'developer-side' ones, like refactoring or intellisense support.

So, no conclusions there - I don't know which way I prefer yet. It seems that I really will have to go on and write some xml-file based initialization library (which will share some of the abovementioned problems of the non-static languages, of course, but renaming those properties in the config by hand which you just modified in the code at least feels a bit more normal).

Still, if you're interested, here's the extension for doing the job:

public static class DataDomainInitializerExtension

{
  public static DataDomainInitializer<T> Init<T>(
      this DataDomain dd, params object[] parameters)
  {
    return new DataDomainInitializer<T>(dd.New<T>(parameters));
  }
}

public class DataDomainInitializer<T>
{
  private readonly T target;
  public DataDomainInitializer(T obj)
  {
    this.target = obj;
  }

  public T As(params Expression<Func<string, object>>[] expressions)
  {
    foreach (Expression<Func<string, object>> expression in expressions)
    {
      object value = GetValue(expression.Body);
      string key = expression.Parameters[0].Name;

      PropertyInfo property = typeof(T).GetProperty(key, 
        BindingFlags.Instance
        |BindingFlags.Public
        |BindingFlags.NonPublic);

      Type collectionType = GetCollectionType(property.PropertyType);
      if (collectionType != null)
      {
        CopyCollection(property, collectionType, value);
      }
      else
      {
        property.SetValue(target, value, null);
      }
    }
    return target;
  }

  private void CopyCollection(
      PropertyInfo property, Type collectionType, object collection)
  {
    object targetProperty = property.GetValue(target, null);

    MethodInfo addMethod = collectionType.GetMethod("Add");
    foreach (object enumValue in (IEnumerable)collection)
    {
      addMethod.Invoke(targetProperty, 
                       new object[] { enumValue });
    }
  }

  private static Type GetCollectionType(Type type)
  {
    foreach (Type @interface in type.GetInterfaces())
      if (@interface.IsGenericType && 
          @interface.GetGenericTypeDefinition() 
            == typeof(ICollection<>))
          return @interface;

     return null;
  }

  private static object GetValue(Expression expression)
  {
     ConstantExpression constExpr = expression as ConstantExpression;
     if (constExpr != null)
       return constExpr.Value;
     return (Expression.Lambda<Func<object>>(expression).Compile())();
  }

}

Posted by Attila.

Genome | Linq
Tuesday, 05 February 2008 13:31:19 (W. Europe Standard Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  |