Skip to content

Commit

Permalink
Created a fast dynamic object to use when reading.
Browse files Browse the repository at this point in the history
Changed ExpandoObjectRecordWriter to be used with any IDictionary<string, object>.
  • Loading branch information
JoshClose committed Apr 25, 2024
1 parent 4c5fe74 commit d3852d3
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 7 deletions.
7 changes: 3 additions & 4 deletions src/CsvHelper/Expressions/DynamicRecordCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// https://github.com/JoshClose/CsvHelper
using System;
using System.Collections.Generic;
using System.Dynamic;

namespace CsvHelper.Expressions
{
Expand Down Expand Up @@ -32,7 +31,7 @@ public DynamicRecordCreator(CsvReader reader) : base(reader) { }
/// </summary>
protected virtual dynamic CreateDynamicRecord()
{
var obj = new ExpandoObject();
var obj = new FastDynamicObject();
var dict = obj as IDictionary<string, object>;
if (Reader.HeaderRecord != null)
{
Expand All @@ -41,7 +40,7 @@ protected virtual dynamic CreateDynamicRecord()
var args = new GetDynamicPropertyNameArgs(i, Reader.Context);
var propertyName = Reader.Configuration.GetDynamicPropertyName(args);
Reader.TryGetField(i, out string field);
dict.Add(propertyName, field);
dict[propertyName] = field;
}
}
else
Expand All @@ -51,7 +50,7 @@ protected virtual dynamic CreateDynamicRecord()
var args = new GetDynamicPropertyNameArgs(i, Reader.Context);
var propertyName = Reader.Configuration.GetDynamicPropertyName(args);
var field = Reader.GetField(i);
dict.Add(propertyName, field);
dict[propertyName] = field;
}
}

Expand Down
49 changes: 49 additions & 0 deletions src/CsvHelper/Expressions/ExpandoObjectRecordWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2009-2024 Josh Close
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper
using System;
using System.Collections.Generic;
using System.Linq;

namespace CsvHelper.Expressions
{
/// <summary>
/// Writes expando objects.
/// </summary>
public class ExpandoObjectRecordWriter : RecordWriter
{
/// <summary>
/// Initializes a new instance using the given writer.
/// </summary>
/// <param name="writer">The writer.</param>
public ExpandoObjectRecordWriter(CsvWriter writer) : base(writer) { }

/// <summary>
/// Creates a <see cref="Delegate"/> of type <see cref="Action{T}"/>
/// that will write the given record using the current writer row.
/// </summary>
/// <typeparam name="T">The record type.</typeparam>
/// <param name="recordType">The record.</param>
protected override Action<T> CreateWriteDelegate<T>(Type recordType)
{
Action<T> action = r =>
{
var dict = ((IDictionary<string, object>)r).AsEnumerable();

if (Writer.Configuration.DynamicPropertySort != null)
{
dict = dict.OrderBy(pair => pair.Key, Writer.Configuration.DynamicPropertySort);
}

var values = dict.Select(pair => pair.Value);
foreach (var val in values)
{
Writer.WriteField(val);
}
};

return action;
}
}
}
4 changes: 2 additions & 2 deletions src/CsvHelper/Expressions/RecordWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ protected virtual Action<T> CreateWriteDelegate<T>(T record)
/// Creates a <see cref="Delegate"/> of type <see cref="Action{T}"/>
/// that will write the given record using the current writer row.
/// </summary>
/// <param name="typeForRecord">The type for the record.</param>
protected abstract Action<T> CreateWriteDelegate<T>(Type typeForRecord);
/// <param name="recordType">The type of the record.</param>
protected abstract Action<T> CreateWriteDelegate<T>(Type recordType);

/// <summary>
/// Combines the delegates into a single multicast delegate.
Expand Down
9 changes: 8 additions & 1 deletion src/CsvHelper/Expressions/RecordWriterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper

using System;
using System.Collections.Generic;
using System.Dynamic;

namespace CsvHelper.Expressions
Expand All @@ -13,6 +13,7 @@ namespace CsvHelper.Expressions
/// </summary>
public class RecordWriterFactory
{
private readonly ExpandoObjectRecordWriter expandoObjectRecordWriter;
private readonly DynamicRecordWriter dynamicRecordWriter;
private readonly PrimitiveRecordWriter primitiveRecordWriter;
private readonly ObjectRecordWriter objectRecordWriter;
Expand All @@ -23,6 +24,7 @@ public class RecordWriterFactory
/// <param name="writer">The writer.</param>
public RecordWriterFactory(CsvWriter writer)
{
expandoObjectRecordWriter = new ExpandoObjectRecordWriter(writer);
dynamicRecordWriter = new DynamicRecordWriter(writer);
primitiveRecordWriter = new PrimitiveRecordWriter(writer);
objectRecordWriter = new ObjectRecordWriter(writer);
Expand All @@ -39,6 +41,11 @@ public virtual RecordWriter MakeRecordWriter(Type recordType)
return primitiveRecordWriter;
}

if (typeof(IDictionary<string, object>).IsAssignableFrom(recordType))
{
return expandoObjectRecordWriter;
}

if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(recordType))
{
return dynamicRecordWriter;
Expand Down
165 changes: 165 additions & 0 deletions src/CsvHelper/FastDynamicObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;

namespace CsvHelper;

internal class FastDynamicObject : IDictionary<string, object>, IDynamicMetaObjectProvider
{
private readonly Dictionary<string, object> dict;

public FastDynamicObject()
{
dict = new Dictionary<string, object>();
}

object IDictionary<string, object>.this[string key]
{
get
{
if (!dict.ContainsKey(key))
{
throw new CsvHelperException($"{nameof(FastDynamicObject)} does not contain a definition for '{key}'.");
}

return dict[key];
}

set
{
dict[key] = value;
}
}

object SetValue(string name, object value)
{
dict[name] = value;

return value;
}

ICollection<string> IDictionary<string, object>.Keys => throw new NotSupportedException();

ICollection<object> IDictionary<string, object>.Values => throw new NotSupportedException();

int ICollection<KeyValuePair<string, object>>.Count => throw new NotSupportedException();

bool ICollection<KeyValuePair<string, object>>.IsReadOnly => throw new NotSupportedException();

DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
{
return new FastDynamicMetaObject(parameter, BindingRestrictions.Empty, this);
}

void IDictionary<string, object>.Add(string key, object value)
{
throw new NotSupportedException();
}

void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}

void ICollection<KeyValuePair<string, object>>.Clear()
{
throw new NotSupportedException();
}

bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}

bool IDictionary<string, object>.ContainsKey(string key)
{
throw new NotSupportedException();
}

void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
throw new NotSupportedException();
}

IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
throw new NotSupportedException();
}

IEnumerator IEnumerable.GetEnumerator()
{
throw new NotSupportedException();
}

bool IDictionary<string, object>.Remove(string key)
{
throw new NotSupportedException();
}

bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}

bool IDictionary<string, object>.TryGetValue(string key, out object value)
{
throw new NotSupportedException();
}

private class FastDynamicMetaObject : DynamicMetaObject
{
private static readonly MethodInfo getValueMethod = typeof(IDictionary<string, object>).GetProperty("Item")!.GetGetMethod()!;
private static readonly MethodInfo setValueMethod = typeof(FastDynamicObject).GetMethod("SetValue", BindingFlags.NonPublic | BindingFlags.Instance)!;

public FastDynamicMetaObject(Expression expression, BindingRestrictions restrictions) : base(expression, restrictions) { }

public FastDynamicMetaObject(Expression expression, BindingRestrictions restrictions, object value) : base(expression, restrictions, value) { }

public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
{
var parameters = new Expression[] { Expression.Constant(binder.Name) };

var callMethod = CallMethod(getValueMethod, parameters);

return callMethod;
}

public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
{
var parameters = new Expression[] { Expression.Constant(binder.Name), Expression.Convert(value.Expression, typeof(object)) };

var callMethod = CallMethod(setValueMethod, parameters);

return callMethod;
}

public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
{
var parameters = new Expression[] { Expression.Constant(binder.Name) };

var callMethod = CallMethod(getValueMethod, parameters);

return callMethod;
}

public override IEnumerable<string> GetDynamicMemberNames()
{
if (HasValue && Value is IDictionary<string, object> lookup)
{
return lookup.Keys;
}

return Array.Empty<string>();
}

private DynamicMetaObject CallMethod(MethodInfo method, Expression[] parameters)
{
var callMethod = new DynamicMetaObject(Expression.Call(Expression.Convert(Expression, LimitType), method, parameters), BindingRestrictions.GetTypeRestriction(Expression, LimitType));

return callMethod;
}
}
}
21 changes: 21 additions & 0 deletions tests/CsvHelper.Tests/Dynamic/CsvRowDynamicObjectTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Xunit;

namespace CsvHelper.Tests.Dynamic
{
public class CsvRowDynamicObjectTests
{
[Fact]
public void Dynamic_SetAndGet_Works()
{
dynamic obj = new FastDynamicObject();
obj.Id = 1;
obj.Name = "one";

var id = obj.Id;
var name = obj.Name;

Assert.Equal(1, id);
Assert.Equal("one", name);
}
}
}

0 comments on commit d3852d3

Please sign in to comment.