jehugaleahsa / FlatFiles

Reads and writes CSV, fixed-length and other flat file formats with a focus on schema definition, configuration and speed.
The Unlicense
357 stars 64 forks source link

Runtime types support #22

Closed silkfire closed 7 years ago

silkfire commented 7 years ago

I was wondering if it would be possible to add support for runtime-generated types.

The backstory is that I have an XML with a list of record structures, each record structure having its own set of fields. In my code, I'm creating instances of runtime-generated types with properties (all of type string) that have a name corresponding to the tag name in the XML.

So far so good. I've even managed to construct (albeit with much effort) the necessary Expression to supply to the Property() method (I'm using type mappers exclusively).

Unfortunately, I get an exception at this line: (https://github.com/jehugaleahsa/FlatFiles/blob/master/src/FlatFiles/TypeMapping/FixedLengthTypeMapper.cs#L960), because obviously, something of type object can't be assigned to my runtime type. I would guess that by perhaps removing this statement, or creating an overload method, we would be able to add support for these kind of dynamic types. I would also think that this would have the added benefit of supporting anonymous types.

jehugaleahsa commented 7 years ago

All that check is doing is making sure the property referred to by the lambda expression is in the class being mapped. It avoids someone saying mapper.Property(x => x.y.z, ...).

I've thought about support for dynamic types in the past. Time to think about it again...

On Oct 18, 2017 3:39 PM, "silkfire" notifications@github.com wrote:

I was wondering if it would be possible to add support for runtime-generated types.

The backstory is that I have an XML with a list of record structures, each record structure having its own set of fields. In my code, I'm creating instances of runtime-generated types with properties (all of type string) that have a name corresponding to the tag name in the XML.

So far so good. I've even managed to construct (albeit with much effort) the necessary Expression to supply to the Property() method (I'm using type mappers exclusively).

Unfortunately, I get an exception at this line: (https://github.com/ jehugaleahsa/FlatFiles/blob/master/src/FlatFiles/TypeMapping/ FixedLengthTypeMapper.cs#L960), because obviously, something of type object can't be assigned to my runtime type. I would guess that by perhaps removing this statement, or creating an overload method, we would be able to add support for these kind of dynamic types. I would believe that this would also add support for anonymous types as well.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/jehugaleahsa/FlatFiles/issues/22, or mute the thread https://github.com/notifications/unsubscribe-auth/ABTgPmEzhbidkaLnACrP9NbWnqvBPag2ks5stlQJgaJpZM4P-QBW .

silkfire commented 7 years ago

Thank you! You should really consider it. Would make this library pure perfection.

Okay, I assume that's why my statement isn't working, because I can't create an Expression of type Func<(runtime type), string>, only Func<object, string>, and the mapping statement as:

mapper.Property(<object> => ((runtime type)<object>)).StringProperty.

And obviously, object can't be assigned to (runtime type).

silkfire commented 7 years ago

Any update on whether this feature is in the works?

jehugaleahsa commented 7 years ago

Interesting fact, you can't pass a lambda to a dynamic call:

dynamic foo = mapper;
foo.Property(x => x.Name);

Expression trees (which is how the type mappers work) are a compile time concept, so are not available at runtime. So even though I can generate a mapper at runtime, I can't call any methods on it to configure it. The code below generates a mapper at runtime that's more or less unusable.

public static dynamic DefineDynamic(Type type, Func<object> factory)
{
    if (factory == null)
    {
        throw new ArgumentNullException(nameof(factory));
    }
    Type mapperType = typeof(SeparatedValueTypeMapper<>).MakeGenericType(type);
    dynamic mapper = Activator.CreateInstance(mapperType, factory);
    return mapper;
}

That all said, what you're really trying to achieve is read/write from properties of a type at runtime. What you need is a way to map columns to properties without using expression trees. I am going to spend some time tomorrow looking into a new interface that will just take property names, rather than expression trees. I already have some code started...

jehugaleahsa commented 7 years ago

Side note, technically there is already partial support in FlatFiles for anonymous types... it's just a little unintuitive. Consider:

var mapper = SeparatedValueTypeMapper.Define(() => new
{
    Name = (string)null
});
mapper.Property(x => x.Name).ColumnName("Name");
StringWriter writer = new StringWriter();
mapper.Write(writer, new[]
{
    new { Name = "John" }, new { Name = "Sam" }
});
string result = writer.ToString();

While you can use this to write anonymous objects to a file, you could never populate anonymous objects this way, because they are read-only. FlatFiles works by first instantiating an object and then setting the properties. I couldn't tell you if anonymous types are using a giant ctor or if the backing fields are written to directly. Switching to an actual class at that point makes a lot more sense.

jehugaleahsa commented 7 years ago

You should see a new NuGet package out there, version 0.3.26: https://www.nuget.org/packages/FlatFiles/

Your code should look something like this:

Type runtimeType = ...;  // Assume this type has a default constructor
var mapper = SeparatedValueTypeMapper.DefineDynamic(runtimeType);
mapper.Int32Property("Id");
mapper.StringProperty("Name");
TextReader reader = ...;  // Assume you are pulling data from somewhere
var records = mapper.Read(reader).ToArray();  // Read all values as IEnumerable<object>
// ... do something
TextWriter writer = ...;  // Assume you are pushing data somewhere
mapper.Write(writer, records);

I can't emphasize enough that there is a lot of new code here and no where near enough unit tests to cover it all yet. That said, I do have some high-level tests and the core functionality appears to be working.

Please let me know if you encounter any odd errors; there is quite a bit of reflection going on under the hood and I know runtime-generated types don't cooperate as expected.

silkfire commented 7 years ago

Thank you so much! I'm impressed that you were able and willing to implement it so quickly.