5
\$\begingroup\$

I'd like to make the usage of my configuration framework easier so I created a few extensions that after getting a value from a source automatically assign it to a property or field. They should make retrieving and assigning setting values as convenient as possible. I'm not concerned about the performance as most of the assignments are one time operations when the application starts.


Introduction

Let me provide a little bit context first, before I show the code for a review. The configuration framework is based on a simple interface that provides just two methods:

public interface IConfiguration
{
    [CanBeNull]
    T GetValue<T>([NotNull] CaseInsensitiveString name);

    void Save([NotNull] CaseInsensitiveString name, [NotNull] object value);
}

I use it with dependency injection and set the values either inside a constructor or while composing an Autofac container (depends on what is more convenient and whether the properties are read-only).

To avoid creating keys manually I use a couple extensions that do that for me. They use expressions to generate the names.

public static class ConfigurationExtensions
{
    public static IConfiguration SetValue<T>(this IConfiguration configuration, Expression<Func<T>> expression)
    {
        var value = configuration.GetValue(expression);
        expression.SetValue(value);
        return configuration;
    }

    public static T GetValue<T>(this IConfiguration config, Expression<Func<T>> expression)
    {
        var memberExpr = expression.Body as MemberExpression ?? throw new ArgumentException("Expression must be a member expression.");
        var name = $"{memberExpr.Member.DeclaringType.Namespace}+{memberExpr.Member.DeclaringType.Name}.{memberExpr.Member.Name}";
        return config.GetValue<T>(name);
    }
}

Review

Now that I have a name and a value it's time to assign it to a property or field. For this I use another extension that uses a little bit of reflection to make this happen. It evaluates the expressions and according to its type sets either a property or a field and it can do this for static and instance classes. The extension can be use outside and inside a class.

public static class MemberSetter
{
    public static void SetValue<T>([NotNull] this Expression<Func<T>> expression, object value)
    {
        if (expression == null) throw new ArgumentNullException(nameof(expression));
        if (expression.Body is MemberExpression memberExpression)
        {
            var obj = GetObject(memberExpression.Expression);

            switch (memberExpression.Member.MemberType)
            {
                case MemberTypes.Property:
                    var property = (PropertyInfo)memberExpression.Member;
                    if (property.CanWrite)
                    {
                        ((PropertyInfo)memberExpression.Member).SetValue(obj, value);
                    }
                    else
                    {
                        var bindingFlags = BindingFlags.NonPublic | (obj == null ? BindingFlags.Static : BindingFlags.Instance);
                        var backingField = (obj?.GetType() ?? property.DeclaringType).GetField($"<{property.Name}>k__BackingField", bindingFlags);
                        if (backingField == null)
                        {
                            throw new BackingFieldNotFoundException(property.Name);
                        }
                        backingField.SetValue(obj, value);
                    }
                    break;
                case MemberTypes.Field:
                    ((FieldInfo)memberExpression.Member).SetValue(obj, value);
                    break;
                default:
                    throw new ArgumentException($"Member must be either a {nameof(MemberTypes.Property)} or a {nameof(MemberTypes.Field)}.");
            }
        }
        else
        {
            throw new ArgumentException($"Expression must be a {nameof(MemberExpression)}.");
        }
    }

    private static object GetObject(Expression expression)
    {
        // This is a static class.
        if (expression == null)
        {
            return null;
        }
        if (expression is MemberExpression anonymousMemberExpression)
        {
            // Extract constant value from the anonyous-wrapper
            var container = ((ConstantExpression)anonymousMemberExpression.Expression).Value;
            return ((FieldInfo)anonymousMemberExpression.Member).GetValue(container);
        }
        else
        {
            return ((ConstantExpression)expression).Value;
        }
    }
}

Tests

Here are a couple of test I wrote.

    [TestMethod]
    public void Load_InstanceMembers_OnTheType_Loaded()
    {
        var config = new Configuration(new Memory
        {
            { "PublicProperty", "a" },
            { "PrivateProperty", "b" },
            { "PublicField", "c" },
            { "PrivateField", "d" },
            { "PrivateReadOnlyField", "e" },
        });

        var x = new InstanceClass(config);

        config.SetValue(() => x.PublicProperty);
        config.SetValue(() => x.PublicField);
        config.SetValue(() => x.PublicReadOnlyProperty);

        CollectionAssert.AreEqual(new[] { "a", null, "c", null, null, "f" }, x.GetValues().ToList());
    }

    [TestMethod]
    public void Load_InstanceMembers_InsideConstructor_Loaded()
    {
        var config = new Configuration(new Memory
        {
            { "PublicProperty", "a" },
            { "PrivateProperty", "b" },
            { "PublicField", "c" },
            { "PrivateField", "d" },
            { "PrivateReadOnlyField", "e" },
            { "PublicReadOnlyProperty", "f" },
        });

        var x = new InstanceClass(config);

        CollectionAssert.AreEqual(new[] { "a", "b", "c", "d", "e", "f" }, x.GetValues().ToList());
    }

    [TestMethod]
    public void Load_StaticMembers_Loaded()
    {
        var config = new Configuration(new Memory
        {
            { "PublicProperty", "a" },
            { "PrivateProperty", "b" },
            { "PublicField", "c" },
            { "PrivateField", "d" },
            { "PrivateReadOnlyField", "e" },
            { "PublicReadOnlyProperty", "f" },
        });

        config.SetValue(() => StaticClass.PublicProperty);
        config.SetValue(() => StaticClass.PublicField);
        config.SetValue(() => StaticClass.PublicReadOnlyProperty);

        CollectionAssert.AreEqual(new[] { "a", null, "c", null, null, "f" }, StaticClass.GetValues().ToList());
    }

    public class InstanceClass
    {
        public InstanceClass() { }

        public InstanceClass(IConfiguration config)
        {
            config.SetValue(() => PublicProperty);
            config.SetValue(() => PrivateProperty);
            config.SetValue(() => PublicField);
            config.SetValue(() => PrivateField);
            config.SetValue(() => PrivateReadOnlyField);
            config.SetValue(() => PublicReadOnlyProperty);
        }

        public string PublicProperty { get; set; }

        private string PrivateProperty { get; set; }

        public string PublicField;

        private string PrivateField;

        private readonly string PrivateReadOnlyField;

        public string PublicReadOnlyProperty { get; }

        public IEnumerable<object> GetValues()
        {
            yield return PublicProperty;
            yield return PrivateProperty;
            yield return PublicField;
            yield return PrivateField;
            yield return PrivateReadOnlyField;
            yield return PublicReadOnlyProperty;
        }
    }

    public static class StaticClass
    {
        public static string PublicProperty { get; set; }

        private static string PrivateProperty { get; set; }

        public static string PublicField;

        private static string PrivateField;

        private static readonly string PrivateReadOnlyField;

        public static string PublicReadOnlyProperty { get; }

        public static IEnumerable<object> GetValues()
        {
            yield return PublicProperty;
            yield return PrivateProperty;
            yield return PublicField;
            yield return PrivateField;
            yield return PrivateReadOnlyField;
            yield return PublicReadOnlyProperty;
        }
    }

I'm mostly interested about:

  • What do you think of this design?
  • Can it more convenient?
  • Is it easy to use and understand?
  • Is the code clean enough?
\$\endgroup\$
3
\$\begingroup\$

This still looks fairly tedious:

config.SetValue(() => x.PublicProperty);
config.SetValue(() => x.PublicField);
config.SetValue(() => x.PublicReadOnlyProperty);

If you are going with reflection, I'd go all the way and implement automatic serialization/deserialization.

//pseudocode for property deserialization
var targetObject = ...;
foreach(var property in targetObject.GetType().GetProperties())
{
    if (!property.CanWrite()) continue;
    var key = ... ; //generate key
    if (!config.HasKey(key)) continue;
    var value = config.GetValue(key);

    property.SetValue(targetObject, value);
}

A few problems that arise:

  1. You can no longer specifically select which members to serialize/deserialize. But this problem can be solved by attributes (similar to [XmlIgnore]).

  2. You either need a non-generic IConfiguration.GetValue, or you will have to use reflection in order to call it.

  3. You need a way to check whether the key is present in configuration.

P.S. I'm not sure I like the idea of messing with private fields/properties. It straight up breaks encapsulation, it is no longer just about convenience.

\$\endgroup\$
  • \$\begingroup\$ I'm considering automatic serialization/deserialization with attributes and default values and validation as the next step ;-) but I need it also to work manually (at first) because if I want to set some properties of classes that I don't own like a Form I need to be able to do that. Setting private fields should make writing Foo = config.GetValue(() => Foo) unnecessary. The config implementation is already checking if a key exists or not and doing other magic ;-) \$\endgroup\$ – t3chb0t Jul 26 '17 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.