Updating lists from defaults using LINQ and reflection

Recently, I have found myself loading a number of XML configuration files containing lists that were subject to change as our application evolved. We needed to be able to smoothly fuse the new default items into the configuration file without losing an changes already performed on items that were still valid list members. In addition, any newly invalidated list members needed to be removed. Using LINQ's Union and Intersect methods, this is actually quite easy.

In our use case, we have a class ReadoutSource whose properties are being regularly updated. The getters for these properties are hooked by reflection, and captured in closures in a list of Readout objects that can be invoked by readonly properties that are bound to the UI. In this example, the other properties on the Readout are a display name and a visible flag. We use the display name as an identifier and the visible flag is set by the user to customize which readouts that they see.

The trick is generating new lists or updating the existing saved list after adding more properties have been added to ReadoutSource. By creating custom attributes and placing them on our target properties, we can associate the default value with the property directly. The last step is where the magic happens. By creating a Union of the existing collection with the defaults, we add any missing defaults to the existing Readout list. Then, we remove any old properties that have been removed from ReadoutSource via Intersect. This works because the first collection in both Union and Intersect is given priority. This means that if an object exists in both the existing Readout list and the default Readout list, the one in the existing Readout list is always kept. In order to ensure that two Readouts are properly evaluated for "equality", you will want to create a class that implements IEqualityComparer<T> to only consider the individual properties that determine equality for your purposes. In our case, equality is determined only by the DisplayName property. See the code below for a full implementation of this example.

public virtual List<Readout> UpdateReadouts(List<Readout> readouts, ReadoutSource readoutSource)
{
    var defaultReadouts = new List<Readout>();

    foreach (var propertyInfo in readoutSource.GetType().GetProperties())
    {
        var defaultNameAttr = propertyInfo.GetCustomAttributes(typeof(DefaultNameAttribute), false).Cast<DefaultNameAttribute>().SingleOrDefault();

        if (defaultNameAttr == null) continue;

        var info = propertyInfo;
        var readoutMetadata = new Readout
        {
            DisplayName = defaultNameAttr.Name,
            ValueGetter = () => info.GetValue(readoutSource, new object[] { }),
        };

        var loadedReadout = readouts.SingleOrDefault(x => x.DisplayName == defaultNameAttr.Name);
        if (loadedReadout != null)
            loadedReadout.ValueGetter = () => info.GetValue(readoutSource, new object[] { });

        defaultReadouts.Add(readoutMetadata);
    }
    var comparer = new ReadoutComparer();
    return readouts.Union(defaultReadouts, comparer).ToList().Intersect(defaultReadouts, comparer).ToList();
}

public class Readout
{
    public string DisplayName { get; set; }
    public bool IsVisible { get; set; }
    public Func<dynamic> ValueGetter { get; set; }
    public dynamic Value { get { return ValueGetter != null ? ValueGetter.Invoke() : null; } }
}

public class ReadoutSource
{
    [DefaultName("My Readout")]
    public double MyReadout { get; set; }
}

public class DefaultNameAttribute : Attribute
{
    public string Name { get; set; }

    public DefaultNameAttribute(string name)
    {
        Name = name;
    }
}

public class ReadoutComparer : IEqualityComparer<Readout>
{
    public bool Equals(Readout x, Readout y)
    {
        return x != null && y != null && x.DisplayName == y.DisplayName;
    }

    public int GetHashCode(Readout obj)
    {
        return obj.DisplayName.GetHashCode();
    }
}
.NETLINQreflectionquick tips