Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Looking for a fast and easy way to coalesce all properties on a POCO

Tags:

c#

reflection

I've got some plain-old classes with a bunch of simple properties (simple {get; set;} declarations). All the properties are nullable (or equivalently, reference types).

For example:

class POCO
{
  int? Field1 { get; set; }
  string Field2 { get; set; }
  ... etc ...
}

I've got a scenario where I'm building these POCOs piecemeal, and at the end I'd like to get one of them with all the non-null fields.

Some illustrative code:

POCO o1 = LoadFields1To3();
POCO o2 = LoadFields4To5();
POCO o3 = LoadFields6To9();
... etc ...

We're in this scenario because some of the fields are loaded from SQL (and sometimes distinct queries), while some are loaded from in memory data structures. I'm re-using the POCO type here to avoid a bunch of otherwise pointless classes (a static type being quite useful for Dapper, and just in general).

What I'm looking for is a nice way to coalesce these objects' properties into a single one with the non-null properties.

Something like:

POCO final = o1.UnionProperties(o2).UnionProperties(o3) // and so on

I am able to guarantee that no field is non-null on more than one object. Though I'd assume a solution would take the left-most non-null field, it's not actually necessary.

I know I can write some reflection code to do this, but it's a bit nasty and slow.

This does need to be generically applicable, as while I never intend to coalesce objects of different types, there are a very large number of types that this method would be applicable to.

I was wondering if there isn't some cleverer way, perhaps abusing dynamic?

like image 502
Kevin Montrose Avatar asked Sep 14 '11 20:09

Kevin Montrose


1 Answers

I gather (ok, I asked you) that the key objectives here are:

  • performance (reflection seeming too slow)
  • low maintenance (want to avoid a very manual copy-method, or complicated properties)

The following uses meta-programming to do whatever it can on the fly at runtime, compiling itself to a typed delegate (Action<POCO, POCO>) for efficient re-use:

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

public class SamplePoco
{
    public int? Field1 { get; set; }
    public string Field2 { get; set; }
    // lots and lots more properties here

}
static class Program
{
    static void Main()
    {
        var obj1 = new SamplePoco { Field1 = 123 };
        var obj2 = new SamplePoco { Field2 = "abc" };
        var merged = Merger.Merge(obj1, obj2);
        Console.WriteLine(merged.Field1);
        Console.WriteLine(merged.Field2);
    }
}

static class Merger
{
    public static T Merge<T>(params T[] sources) where T : class, new()
    {
        var merge = MergerImpl<T>.merge;
        var obj = new T();
        for (int i = 0; i < sources.Length; i++) merge(sources[i], obj);
        return obj;
    }
    static class MergerImpl<T> where T : class, new()
    {
        internal static readonly Action<T, T> merge;

        static MergerImpl()
        {
            var method = new DynamicMethod("Merge", null, new[] { typeof(T), typeof(T) }, typeof(T));
            var il = method.GetILGenerator();

            Dictionary<Type, LocalBuilder> locals = new Dictionary<Type, LocalBuilder>();
            foreach (var prop in typeof(T).GetProperties())
            {
                var propType = prop.PropertyType;
                if (propType.IsValueType && Nullable.GetUnderlyingType(propType) == null)
                {
                    continue; // int, instead of int? etc - skip
                }
                il.Emit(OpCodes.Ldarg_1); // [target]
                il.Emit(OpCodes.Ldarg_0); // [target][source]
                il.EmitCall(OpCodes.Callvirt, prop.GetGetMethod(), null); // [target][value]
                il.Emit(OpCodes.Dup); // [target][value][value]
                Label nonNull = il.DefineLabel(), end = il.DefineLabel();
                if (propType.IsValueType)
                { // int? etc - Nullable<T> - hit .Value
                    LocalBuilder local;
                    if (!locals.TryGetValue(propType, out local))
                    {
                        local = il.DeclareLocal(propType);
                        locals.Add(propType, local);
                    }
                    // need a ref to use it for the static-call
                    il.Emit(OpCodes.Stloc, local); // [target][value]
                    il.Emit(OpCodes.Ldloca, local); // [target][value][value*]
                    var hasValue = propType.GetProperty("HasValue").GetGetMethod();
                    il.EmitCall(OpCodes.Call, hasValue, null); // [target][value][value.HasValue]
                }
                il.Emit(OpCodes.Brtrue_S, nonNull); // [target][value]
                il.Emit(OpCodes.Pop); // [target]
                il.Emit(OpCodes.Pop); // nix
                il.Emit(OpCodes.Br_S, end); // nix
                il.MarkLabel(nonNull); // (incoming) [target][value]
                il.EmitCall(OpCodes.Callvirt, prop.GetSetMethod(), null); // nix
                il.MarkLabel(end); // (incoming) nix
            }
            il.Emit(OpCodes.Ret);
            merge = (Action<T, T>)method.CreateDelegate(typeof(Action<T, T>));
        }
    }
}
like image 117
Marc Gravell Avatar answered Sep 28 '22 15:09

Marc Gravell