Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET 7 AOT Compatibility #2089

Open
enricobenedos opened this issue Nov 22, 2022 · 12 comments
Open

.NET 7 AOT Compatibility #2089

enricobenedos opened this issue Nov 22, 2022 · 12 comments
Labels

Comments

@enricobenedos
Copy link

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
@JoshClose
Copy link
Owner

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
Copy link
Owner

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.

@enricobenedos
Copy link
Author

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.

@JoshClose
Copy link
Owner

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

@enricobenedos
Copy link
Author

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.

@JoshClose
Copy link
Owner

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

@JoshClose
Copy link
Owner

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
Copy link
Owner

JoshClose commented Nov 23, 2022

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

@JoshClose
Copy link
Owner

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
Copy link
Owner

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.

@enricobenedos
Copy link
Author

It really seems a big work to do 😅

@dameng324
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants