3
\$\begingroup\$

Originally created for this answer on SO, I thought there was still room for improvement on the initial basic idea.

Also thought about using an extension method to make using syntax simpler.

Here is an updated version of the dynamic object wrapper. refined for this answer

public static class DynamicObjectWrapperExtension {
    /// <summary>
    /// Return provided object as a <seealso cref="System.Dynamic.DynamicObject"/>
    /// </summary>  
    public static dynamic AsDynamicObject(this object value) {
        return new DynamicObjectWrapper(value);
    }
}

public class DynamicObjectWrapper : DynamicObject, IEquatable<DynamicObjectWrapper> {
    private readonly object value;
    private readonly Type valueType;

    public DynamicObjectWrapper(object value) {
        this.value = value;
        this.valueType = value.GetType();
    }

    public override IEnumerable<string> GetDynamicMemberNames() {
        return valueType.GetProperties().Select(p => p.Name);
    }

    public override bool TryConvert(ConvertBinder binder, out object result) {
        result = null;
        try {
            result = changeTypeCore(value, binder.Type);
        } catch {
            return false;
        }
        return true;
    }

    private object changeTypeCore(object value, Type convertionType) {
        if (ReferenceEquals(value, null))
            return getDefaultValueForType(convertionType);

        var providedType = valueType;
        if (convertionType.IsAssignableFrom(providedType)) {
            return value;
        }

        try {
            var converter = TypeDescriptor.GetConverter(convertionType);
            if (converter.CanConvertFrom(providedType)) {
                return converter.ConvertFrom(value);
            }

            converter = TypeDescriptor.GetConverter(providedType);
            if (converter.CanConvertTo(providedType)) {
                return converter.ConvertTo(value, convertionType);
            }
        } catch {
            return value;
        }

        try {
            return Convert.ChangeType(value, convertionType, System.Globalization.CultureInfo.CurrentCulture);
        } catch {
            return value;
        }
    }

    private object getDefaultValueForType(Type targetType) {
        return targetType.IsClass || targetType.IsInterface ? null : Activator.CreateInstance(targetType);
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) {
        result = null;
        //1d collection
        if (potentialIndex(indexes)) {
            int index = (int)indexes[0];
            var list = value as IList;
            if (validIndex(index, list)) {
                result = checkValue(list[index]);
                return true;
            }
        }
        return false;
    }

    private bool validIndex(int index, IList list) {
        return index >= 0 && index < list.Count;
    }

    private bool potentialIndex(object[] indexes) {
        return indexes[0] != null && typeof(int) == indexes[0].GetType() && value is IList;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result) {
        return TryGetValue(binder.Name, out result);
    }

    public bool TryGetValue(string propertyName, out object result) {
        result = null;
        var property = valueType.GetProperty(propertyName);
        if (property != null) {
            var propertyValue = property.GetValue(value, null);
            result = checkValue(propertyValue);
            return true;
        }
        return false;
    }

    private object checkValue(object value) {
        var valueType = value.GetType();
        return isAnonymousType(valueType)
            ? new DynamicObjectWrapper(value)
            : value;
    }

    private bool isAnonymousType(Type type) {
        //HACK: temporary hack till a proper function can be implemented
        return type.Namespace == null &&
            type.IsGenericType &&
            type.IsClass &&
            type.IsSealed &&
            type.IsPublic == false;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) {
        try {
            result = valueType.InvokeMember(
                binder.Name,
                BindingFlags.InvokeMethod |
                BindingFlags.Public |
                BindingFlags.Instance,
                null, value, args);

            return true;
        } catch {
            result = null;
            return false;
        }
    }

    public override bool Equals(object obj) {
        // If parameter is null return false.
        if (ReferenceEquals(obj, null)) return false;

        // Return true if the fields match:
        return this.value == obj || (obj is DynamicObjectWrapper && Equals(obj as DynamicObjectWrapper));
    }

    public bool Equals(DynamicObjectWrapper other) {
        // If parameter is null return false.
        if (ReferenceEquals(other, null)) return false;
        // Return true if the fields match:
        return this.value == other.value;
    }

    public override int GetHashCode() {
        return ToString().GetHashCode();
    }

    public override string ToString() {
        var name = GetType().Name;
        return string.Format("{0}[{1}]", name, value);
    }

}

Assuming the following controller

public class FooController : Controller {

    public IActionResult GetAnonymousObject() {

        var jsonResult = new {
            id = 1,
            name = "Foo",
            type = "Bar"
        };

        return Json(jsonResult);
    }

    public IActionResult GetAnonymousCollection() {

        var jsonResult = Enumerable.Range(1, 20).Select(x => new {
            id = x,
            name = "Foo" + x,
            type = "Bar" + x
        }).ToList();

        return Json(jsonResult);
    }
}

Usage examples

[TestClass]
public class DynamicObjectWrapperTests {
    [TestMethod]
    public void DynamicObjectResultValue_Member_Should_Exist() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousObject() as JsonResult;

        //Assert
        dynamic obj = result.Value.AsDynamicObject();

        Assert.IsNotNull(obj);
        Assert.AreEqual(1, obj.id);
        Assert.AreEqual("Foo", obj.name);
        Assert.AreEqual(3, obj.name.Length);
        Assert.AreEqual("Bar", obj.type);
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;

        //Assert
        dynamic jsonCollection = result.Value;
        foreach (object value in jsonCollection) {
            dynamic json = value.AsDynamicObject();

            Assert.IsNotNull(json.id,
                "JSON record does not contain \"id\" required property.");
            Assert.IsNotNull(json.name,
                "JSON record does not contain \"name\" required property.");
            Assert.IsNotNull(json.type,
                "JSON record does not contain \"type\" required property.");
        }
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Should_Convert_To_IEnumerable() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;
        dynamic jsonCollection = result.Value.AsDynamicObject();
        int count = 0;
        foreach (var value in jsonCollection) {
            count++;
        }

        //Assert
        Assert.IsTrue(count > 0);
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Index_at_0_Should_Not_be_Null() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;
        dynamic jsonCollection = result.Value.AsDynamicObject();

        //Assert                
        Assert.IsNotNull(jsonCollection[0]);
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Should_Be_Indexable() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;
        dynamic jsonCollection = result.Value.AsDynamicObject();

        //Assert
        for (var i = 0; i < jsonCollection.Count; i++) {

            var json = jsonCollection[i];

            Assert.IsNotNull(json);
            Assert.IsNotNull(json.id,
               "JSON record does not contain \"id\" required property.");
            Assert.IsNotNull(json.name,
                "JSON record does not contain \"name\" required property.");
            Assert.IsNotNull(json.type,
                "JSON record does not contain \"type\" required property.");

        }
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Count_Should_Be_20() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;

        //Assert
        dynamic jsonCollection = result.Value.AsDynamicObject();

        Assert.AreEqual(20, jsonCollection.Count);
    }

}
\$\endgroup\$
  • \$\begingroup\$ Could you mark the line or give an example where the type conversion is needed? I don't think you need the TryConvert. \$\endgroup\$ – t3chb0t Jul 24 '16 at 14:24
  • \$\begingroup\$ when used in a foreach the framework tries to convert it to an IEnumerable. The third test method tests that case. \$\endgroup\$ – Nkosi Jul 24 '16 at 14:37
  • \$\begingroup\$ ok, this makes sense but your converter seems to be able to do much more then that. why don't you just support the IEnumerable with binder.Type == typeof(IEnumerable)? wouldn't that be enough? btw - this foreach (var element in obj.AsDynamicObject()) doesn't work and throws an exception. \$\endgroup\$ – t3chb0t Jul 24 '16 at 14:41
  • \$\begingroup\$ @t3chb0t I was originally returning only dynamic object wrapper when getting members which is why the convert looked so verbose. I was trying to allow for more complex cases, but that affected usability as it then meant I had to cast all member access. like eg Assert.AreEqual(1, (int)json.id) which would call the convert as well when I figured most users would want to just have Assert.AreEqual(1, json.id) without having to do the extra cast. \$\endgroup\$ – Nkosi Jul 24 '16 at 14:46
  • \$\begingroup\$ @t3chb0t do you have any suggestion on which way to go or is that too opinion based? \$\endgroup\$ – Nkosi Jul 24 '16 at 14:47
1
\$\begingroup\$

This is a really interesting solution. However, I don’t really see how this is ultimately useful. If I wanted to unit test an anonymous type, instead of testing if all properties exist on it all the time, changing it to return a static type sounds a lot easier to me.

If you cannot change the return type, for example because you have multiple different possible return types, I would still prefer to write the tests with static types. For this purpose, you could define the type in the test and then map those anonymous types to the known static type.

For example, something like this:

public T MapTo<T>(object obj, bool strict = false)
    where T : new()
{
    var sourceProperties = obj.GetType().GetProperties();
    var sourcePropertyNames = new HashSet<string>(sourceProperties.Select(p => p.Name));

    var targetProperties = typeof(T).GetProperties().ToDictionary(p => p.Name);
    var targetPropertyNames = new HashSet<string>(targetProperties.Values.Select(p => p.Name));

    var extraneousProperties = sourcePropertyNames.Except(targetPropertyNames);
    if (extraneousProperties.Any())
        Assert.Fail(string.Format("Object contains unexpected properties: {0}", string.Join(", ", extraneousProperties)));

    if (strict) {
        var missingProperties = targetPropertyNames.Except(sourcePropertyNames);
        if (missingProperties.Any())
            Assert.Fail(string.Format("Object is missing properties: {0}", string.Join(", ", missingProperties)));
    }

    var target = new T();
    foreach (var pi in sourceProperties) {
        var tpi = targetProperties[pi.Name];
        if (strict && pi.PropertyType != tpi.PropertyType)
            Assert.Fail(string.Format("Property {0} has the wrong type (is {1}, should be {2})", pi.Name, pi.PropertyType, tpi.PropertyType));
        else if (!tpi.PropertyType.IsAssignableFrom(pi.PropertyType))
            Assert.Fail(string.Format("Property {0} has incompatible type (is {1}, should be assignable to {2})", pi.Name, pi.PropertyType, tpi.PropertyType));

        tpi.SetValue(target, pi.GetValue(obj));
    }

    return target;
}

It could then be used in a test like this:

[TestMethod]
public void GetAnonymousObject_ReturnsRightProperties() {
    // arrange
    var controller = new FooController();

    // act
    var result = controller.GetAnonymousObject() as JsonResult;

    // assert
    Assert.IsNotNull(result.Data);
    var obj = MapTo<TargetType>(result.Data, strict: true);

    Assert.AreEqual(1, obj.id);
    Assert.AreEqual("Foo", obj.name);
    Assert.AreEqual("Bar", obj.type);
}

class TargetType {
    public int id { get; set; }
    public string name { get; set; }
    public string type { get; set; }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ To me the purpose of the dynamic object wrapper is to avoid having a DTO object like the TargetType which sometimes might be very helpful. I wouldn't like having to create a DTO object everytime. \$\endgroup\$ – t3chb0t Jul 24 '16 at 16:38

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.