CsvHelper
CsvHelper copied to clipboard
.NET 7 AOT Compatibility
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
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.
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.
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.
Oh interesting. Do you have experience with source generators if I have questions?
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.
Awesome thanks! I bet AOT will allow for all feature to work on iOS now too.
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.
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
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.
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.
It really seems a big work to do 😅
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.