Skip to content

Support Auto Mapping Dynamics

Travis Parks edited this page Jul 3, 2018 · 1 revision

With FlatFiles 3.0, it is easier than ever getting started. You can use the new GetAutoMappedReader and GetAutoMappedWriter methods to read and write delimited files without any configuration necessary. I was hesitant to introduce these new features: the core values of FlatFiles is configuration and speed. With the new CustomMapping feature, it was finally possible to efficiently build and use the schema at runtime. If you can get away with not defining a schema, it ultimately means less code you need to maintain for some minor upfront overhead.

We can take auto-mapping a little further and allow mapping into a dynamic type. In this scenario, we let the schema drive everything. The only thing we can't definitely determine from the source file is the type of the columns. That's what makes type mapping so power: it fills in that last piece of missing information. Most of the time, however, we can determine what type of field should be based on its usage.

For example, a future version of FlatFiles might support calling GetAutoMappedReader without generic arguments, in which case a ITypedReader<dynamic> would be returned. Coming from a file, out-of-the-box all of the properties would be string values. However, without too much effort, I could write a subclass of DynamicObject that could detect when you try to assign to an int or DateTime or Guid. In which case, it would try to convert the string to the appropriate type. For those nasty edge cases, you could explicitly cast to string and pass it to DateTime.ParseExact or Guid.TryParse.

I wanted to play around with what that code might look like:

    internal class DynamicEntity : DynamicObject
    {
        private readonly Dictionary<string, object> members;
        private readonly Dictionary<int, string> positions;

        public DynamicEntity(bool isCaseSensitive = false)
        {
            var comparer = isCaseSensitive 
                ? EqualityComparer<string>.Default
                : (IEqualityComparer<string>)StringComparer.OrdinalIgnoreCase;
            members = new Dictionary<string, object>(comparer);
            positions = new Dictionary<int, string>();
        }

        public override bool TryDeleteIndex(DeleteIndexBinder binder, object[] indexes)
        {
            if (indexes.Length == 1)
            {
                if (indexes[0] is string memberName)
                {
                    return RemoveMember(memberName);
                }
                else if (indexes[0] is int position && positions.TryGetValue(position, out memberName))
                {
                    return RemoveMember(memberName);
                }
            }
            return base.TryDeleteIndex(binder, indexes);
        }

        public override bool TryDeleteMember(DeleteMemberBinder binder)
        {
            return RemoveMember(binder.Name);
        }

        public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
        {
            if (indexes.Length == 1)
            {
                if (indexes[0] is string memberName)
                {
                    return members.TryGetValue(memberName, out result);
                }
                else if (indexes[0] is int position && positions.TryGetValue(position, out memberName))
                {
                    return members.TryGetValue(memberName, out result);
                }
            }
            return base.TryGetIndex(binder, indexes, out result);
        }

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

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            if (members.TryGetValue(binder.Name, out object member) && member is Delegate delegateMember)
            {
                result = DynamicMember.Create(delegateMember.DynamicInvoke(args));
                return true;
            }
            return base.TryInvokeMember(binder, args, out result);
        }

        public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
        {
            if (indexes.Length == 1)
            {
                if (indexes[0] is string memberName)
                {
                    SetMember(memberName, value);
                    return true;
                }
                else if (indexes[0] is int position && positions.TryGetValue(position, out memberName))
                {
                    SetMember(memberName, value);
                }
            }
            return base.TrySetIndex(binder, indexes, value);
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            SetMember(binder.Name, value);
            return true;
        }

        private void SetMember(string name, object value)
        {
            var member = DynamicMember.Create(value);
            members[name] = member;
            positions[positions.Count] = name;
        }

        private bool RemoveMember(string name)
        {
            if (members.Remove(name))
            {
                int position = positions.Where(p => p.Value == name).Select(p => p.Key).Single();
                positions.Remove(position);
                return true;
            }
            else
            {
                return false;
            }
        }
    }

    internal class DynamicMember : DynamicObject
    {
        private readonly object value;

        public DynamicMember(object value)
        {
            this.value = value;
        }

        internal static object Create(object value)
        {
            if (value != null && value is IConvertible)
            {
                return new DynamicMember(value);
            }
            return value;
        }

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

        private static object ConvertTo(Type type, object value)
        {
            var actualType = Nullable.GetUnderlyingType(type) ?? type;
            return Convert.ChangeType(value, actualType);
        }

        public override bool TryUnaryOperation(UnaryOperationBinder binder, out object result)
        {
            try
            {
                var argType = value.GetType();
                var arg = Expression.Parameter(argType, "value");
                var operation = Expression.MakeUnary(binder.Operation, arg, argType);
                var lambda = Expression.Lambda(operation, arg);
                var compiledDelegate = lambda.Compile();
                var returnValue = compiledDelegate.DynamicInvoke(this.value);
                result = Create(returnValue);
                return true;
            }
            catch
            {
            }
            result = value;
            return true;
        }

        public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result)
        {
            if (arg != null)
            {
                try
                {
                    var argType = arg.GetType();
                    var converted = ConvertTo(argType, value);
                    var left = Expression.Parameter(argType, "left");
                    var right = Expression.Parameter(argType, "right");
                    var operation = Expression.MakeBinary(binder.Operation, left, right);
                    var lambda = Expression.Lambda(operation, left, right);
                    var compiledDelegate = lambda.Compile();
                    var returnValue = compiledDelegate.DynamicInvoke(converted, arg);
                    result = Create(returnValue);
                    return true;
                }
                catch
                {
                }
            }
            result = value;
            return true;
        }

        public override string ToString()
        {
            return value.ToString();
        }
    }
Clone this wiki locally