An big enough application is likely going to depend on third-party libraries that may not have been audited for AOT safety. Some of these libraries may work out of the box while others may be problematic. There are generally few options that can be employed to attach the issue:
- Update the library to be AOT safe and run it through AOT analyzer. This may be the preferred option if the library is open source. It will likely help other users of the library as well.
- Use alternative library that is AOT safe. Many popular libraries have forks or alternatives that are built with AOT in mind. For example, there's DapperAOT for Dapper, NextGenMapper for Automapper (not source compatible), etc.
- Mark the whole library as rooted and not trimmed. This may help alleviate some issues but certainly not all of them.
The general rule of thumb is to start with marking assemblies with <IsAotCompatible>true</IsAotCompatible>
in the project files. This will enable the AOT and single-file analyzers and start
warning on problematic APIs.
Typically you will encounter usage of some APIs like Enum.GetValues(Type)
that
come with newer generic-shaped alternatives like Enum.GetValues<T>()
. This can
be trivially changed as long as the library targets the latest TargetFramework
version. If a down-level compatibility is necessary, wrapping inside #if NET80_OR_GREATER
conditional preprocessor directive is advised.
There's a class of problemes, like the JSON serialization, that is solved with source generators. These generators run at compile time and generate data and methods necessary to avoid reflection at runtime, or at least to scope the reflection to types and methods that can be statically analyzed by the AOT compiler.
Updating JSON serialization to use source generators requires to essentially do two steps:
- Identify which classes/structs participate in the JSON (de)serialization and
create
JsonSerializerContext
derived class for them with theJsonSerializable
attribute for each type. - When doing the serialization itself, use the method overloads that take the
JsonTypeInfo<T>
extra parameter. Don't worry, the AOT analyzer will flag this for you with appropriate warnings.
And example of the serialization context class would look like this:
using System.Text.Json;
using System.Text.Json.Serialization;
using MyServiceClient.Data;
namespace MyServiceClient
{
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(TransformRequest))]
[JsonSerializable(typeof(TransformReply))]
partial class JsonSourceGenerationContext : JsonSerializerContext
{
}
}
You may notice the JsonSourceGenerationOptions
attribute in the example. If you
used custom JsonSerializerOptions
in calls to JsonSerializer.[De]Serialize[Async]
then in many of the cases the options can be moved to the JsonSourceGenerationOptions
attribute.
Then you just modify your calls to JsonSerializer.Serialize(transformRequest)
to
JsonSerializer.Serialize(transformRequest, JsonSourceGenerationContext.Default.TransformRequest)
.
Common JSON abstractions, such as JsonContent
for HTTP request, or the System.Net.Http.Json
extension methods also come with overloads that include the JsonTypeInfo<T>
parameter.
There's currently no source generator for XML serializers like there's one for JSON. The
options to handle the sitation are limited. We employed twothree different approaches to handle
the problem. In some cases the easiest way is to hand write the serialization using the
XmlReader
/XmlWriter
classes. In other cases we crafted a way to use the SGen tools to
generate the (de)serialization code automatically with minimal refactoring.
For the hand-written parts we took the approach of implementing the IXmlSerializable
on
each class. The interface comes with three methods - ReadXml(XmlReader)
, WriteXml(XmlWriter)
,
and XmlSchema GetSchema()
. For our purposes the last method is irrelevant and can just
return null
. The other two methods then implement strongly typed (de)serialization that
can be called instead of XmlSerializer.[De]Serialize
.
In order to use SGen we need to restructure the code a bit and then use a couple of MSBuild
tricks to automate everything. The first step is to split the classes that we want to
(de)serialize into separate .cs files. In case there's some attached code logic the recommened
approach is to use partial
classes and split the logic into its own file. Once we are done
with that, add a new <ItemGroup>
into the project and list all the files with the pure
serialization classes inside as <SgenCompile Include="MyDataClass.cs">
. Finally, add the
MSBuild magic with <Import Project="XmlSerializerGenerator.targets" />
and make sure that you have the copy of XmlSerializerGenerator.targets
alongside the project.
What does the MSBuild magic do? Good question, it builds a small .dll file just from the
source files listed in the SgenCompile
item group. It then proceeds to run SGen on it and
takes the .cs file from the output. The .cs file gets fixed up to address some warnings and
then it gets included into a compile item for the main project. For example, if you had a
MyDataClass
class then you will get a MyDataClassSerializer
generated class. The new
MyDataClassSerializer
class can now be used in place of XmlSerializer
. This will
still generate code that produces AOT warnings but those can be ignored with a local suppression.
It turns out that the XmlSerializer
still depends on reflection even for the source generated
mapping due to how it internally handles Mode
. ILLink annotations may be the only way to
workaround it for now, and at that point there's very little benefit to using the pregenerated
serializers.
A special mention is deserved for libraries that wrap HTTP REST APIs. These APIs often come with machine readable description in the OpenAPI format or another JSON Schema based specification.
It's common to generate the client SDK for those APIs through an automated generator. There are multiple such generators (NSwag, AutoRest, Refit, Microsoft Kiota, and several others). Notably, these generators differ in the additional dependencies (eg. Newtonsoft.Json vs System.Text.Json) and various implementation details. Some generators are more suitable for AOT than others.
Microsoft Kiota seems to work quite well in the AOT scenarios since serialization and deserialization code is generated as strongly typed source code. It doesn't depend on the reflection-based serialization in the underlying JSON library. It is essential to use the latest version of Kiota libraries though, since AOT specific issues were fixed there.
As a proof of concept, we built a subset of Google API Client SDK
using Microsoft Kiota. The Google Discovery API definitions are first converted into
OpenAPI. Then the yq
tool is used to patch up any problematic spot in the OpenAPI
definition file. Lastly, Kiota is used to generate the client API libraries that are
published as NuGet. All of this is scripted using GitHub Actions to ensure an updated
SDK can be produced seamlessly when the Google API is updated. We tested replacing
the Google provided SDK for Gmail, Google Calendar, Google Tasks, Google Drive APIs
in eM Client and it was mostly seamless. The only
problematic part were the resumable upload APIs for Google Drive which are also
hand-written in the Google provided client libraries.
There are multiple workarounds that you can use for AOT unsafe libraries, but most of them are out of scope for this document.
For cases where excessive code trimming is a problem or fields/methods are accessed by
reflection, you can instruct the compiler to root additional assemblies, classes, fields,
or methods. The way to do so is to add them as trimming roots to the main project with
TrimmerRootAssembly
or TrimmerRootDescriptor
. This is described in the ILLink documentation.
Some technologies, like COM interop on Windows, can be augmented for existing libraries without modifying them. For an example of this approach, see the WinFormsComInterop project.
Libraries that require dynamic assembly loading or rely on dynamic code generarion are fundamentally incompatible with NativeAOT. If you heavily rely on those and have no alternative, then technologies like CoreCLR ReadyToRun or MonoVM may be a better fit. Both of them offer partial AOT augmented by JIT or interpreter to handle the dynamic code.