CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

.NET 7 AOT Compatibility

Open enricobenedos opened this issue 2 years ago • 12 comments

Is your feature request related to a problem? Please describe. .NET 7 has been finally released in November 8th. With the new framework update Microsoft ships also the native AOT compilation feature. For now this feature is supported only on console apps and with some limits explained here.

Is it possible that this library become one of the supported ones for AOT compilation?

Additional context At the moment we simply try to convert one of our console app, that use CsvHelper 29.0.0 from .net6.0 to .net7.0 adding also the new AOT property as in this sample:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Version>1.1.1</Version>
  </PropertyGroup>
...

It seems that CsvHelper is not supported:

Unhandled Exception: System.NotSupportedException: 'CsvHelper.Configuration.DefaultClassMap`1[Yahoo.Shared.Models.YahooHistory]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT. Inspect and fix trimming and AOT related warnings that were generated when the app was published. For more information see https://aka.ms/nativeaot-compatibility
   at System.Reflection.Runtime.General.TypeUnifier.WithVerifiedTypeHandle(RuntimeConstructedGenericTypeInfo, RuntimeTypeInfo[]) + 0x88
   at CsvHelper.CsvContext.AutoMap(Type) + 0x4c
   at CsvHelper.CsvReader.ValidateHeader(Type) + 0x65
   at CsvHelper.CsvReader.<GetRecords>d__87`1.MoveNext() + 0xb3
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection) + 0xed
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1) + 0x3d
   at Shared.CsvUtilities.GetCsvData[T](String, ILogger) + 0x140

enricobenedos avatar Nov 22 '22 13:11 enricobenedos

I'm not sure if this is possible.

https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/warnings/il3050

Nearly every method in CsvHelper needs RequiresDynamicCodeAttribute added to it.

It looks like you might just need to have the type declared somewhere in the system.

In your case, try putting the type DefaultClassMap<YahooHistory> somewhere in your code. Just an empty dummy class.

I'm looking into adding that method everywhere.

JoshClose avatar Nov 22 '22 22:11 JoshClose

Yeah... Pretty much every method in the system needs this added to it. Doesn't seem like the right way to do it. Make a note in the documentation on it instead or something.

JoshClose avatar Nov 22 '22 22:11 JoshClose

Thank you @JoshClose, I'll test your workaround today. Anyway, I think that the right way to solve the problem is to use code source generators and generate custom library code at compile time based on app code.

enricobenedos avatar Nov 23 '22 07:11 enricobenedos

Oh interesting. Do you have experience with source generators if I have questions?

JoshClose avatar Nov 23 '22 13:11 JoshClose

Unfortunately no.. I studied them one year ago but only for "academic" purposes.

I studied some Microsoft docs to better understand the new implementations also for native libraries and here you can find how works the Microsoft implementation for System.Text.Json native library. Hope it helps to study how Microsoft implements that for its own JSON serializer.

enricobenedos avatar Nov 23 '22 14:11 enricobenedos

Awesome thanks! I bet AOT will allow for all feature to work on iOS now too.

JoshClose avatar Nov 23 '22 14:11 JoshClose

Looks like I might be able to do a quick and dirty solution of just creating classes DefaultClassMap<MyType> and it still uses reflection to do everything. I need to confirm that with code though.

The better way to do it would be to do what System.Text.Json does and make the user do a couple steps ahead of time. This info is then passed in and is used instead of reflection. I believe you can't use Expressions to compile code either, so everything would have to be built using generators. There would be an entire stack of code generation for this. I suppose reflection and expression tree compilation could be used in the generators though, so maybe the amount of code won't actually be that much.

I'll have to look into this more.

JoshClose avatar Nov 23 '22 16:11 JoshClose

Looks like using compiled lambda expressions was interpreted in AOT scenarios previously. I'm checking if that is still the case. https://github.com/dotnet/runtime/issues/17973

JoshClose avatar Nov 23 '22 17:11 JoshClose

They're still interpreted which means slower than reflection. The quick dirty solution will make it insanely slow for you. I will need to reimplement that entire library using reflection only (no expression trees). It looks like source generators work on netstandard2.0 though, so it'll go back to net461. I don't know if I want to keep 2 versions of the code though. I'll have to do some more digging to see if I can get away with just using source generators.

JoshClose avatar Nov 23 '22 18:11 JoshClose

The generators are only netstandard2.0 because they just run while compiling with Roslyn. The generator code is in another assembly and can take on any dependencies it wants, separate from the CsvHelper assembly.

Taking a look at this https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/

Do Source Generators introduce compatibility concerns for libraries? This depends on how libraries are authored. Since VB and F# currently don’t support Source Generators, library authors should avoid designing their features such that they require a Source Generator. Ideally, features have fallbacks to runtime reflection and/or reflection emit. This is something that library authors will need to careful consider before adopting Source Generators. We expect most library authors will use Source Generators to augment – rather than replace – current experiences for C# developers.

I can't do source generators only. This will have to be another method, or a parameter passed into existing methods that enable it. Or maybe even a CsvReaderAot and CsvWriterAot or something like that.

This will be a significantly large undertaking.

JoshClose avatar Nov 26 '22 19:11 JoshClose

It really seems a big work to do 😅

enricobenedos avatar Nov 28 '22 07:11 enricobenedos

I successfully used CSVHelper work on native aot by using DynamicDependency. Maybe the experience will help other user.

Demo code:

public class CsvRecord
{
    public string Exchange { get; set; }
    public string Commodity { get; set; }
    public string TradingRange { get; set; }
}

internal class Program
{
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvRecord))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Configuration.DefaultClassMap<CsvRecord>))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Configuration.MemberMap<CsvRecord,string>))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.RecordManager))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.RecordCreatorFactory))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.RecordHydrator))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.ExpressionManager))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.TypeConversion.StringConverter))]
    static void Main(string[] args)
    {
        string csvString = """
            Exchange,Commodity,TradingRange
            GX,USD/JPY,0.01
            GX,EUR/JPY,0.01
            """;
        StringReader stringReader = new StringReader(csvString);
        CsvHelper.CsvReader csvReader = new CsvHelper.CsvReader(
            stringReader,
            System.Globalization.CultureInfo.InvariantCulture
        );
        var record = csvReader.GetRecords<CsvRecord>();
        foreach (var r in record)
        {
            Console.WriteLine(r.Exchange);
            Console.WriteLine(r.Commodity);
            Console.WriteLine(r.TradingRange);
        }
    }
}

I use these Attribute to able run CSVHelper at AOT mode.

[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvRecord))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Configuration.DefaultClassMap<CsvRecord>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Configuration.MemberMap<CsvRecord,string>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.RecordManager))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.RecordCreatorFactory))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.RecordHydrator))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.Expressions.ExpressionManager))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CsvHelper.TypeConversion.StringConverter))]

The main point: Adding CsvHelper.Configuration.MemberMap<CsvRecord,string> and CsvHelper.TypeConversion.StringConverter because CsvRecord has string field, If has int Type ,add their MemberMap<string,int> and Int32Converter on it. double/decimal is similar as it.

I hope this can help you.

dameng324 avatar Sep 27 '23 09:09 dameng324