I. Introduction
The webmaster has been working with AOT for 3 months now, previously mentioned in the article "Good News: Breakthrough in .NET 9 X86 AOT - Supports Old Win7 and XP Environments". During this time, the webmaster's project developed with Avalonia also successfully completed AOT publishing tests. However, this process was not smooth sailing. The webmaster only started AOT testing after most of the project's functionality was complete, encountering quite a few problems along the way – a "pitfall-filled" experience. To facilitate future reference and to provide guidance for readers, here is a summary of this experience.
.NET AOT is a technology that pre-compiles .NET code into native code. It offers many advantages: fast startup, reduced runtime resource usage, and improved security. After AOT publishing, there is no need to install the .NET runtime or other dependencies. After .NET 8 and 9 AOT publishing, applications can run under XP and Win7 non-SP1 operating systems. This makes application deployment more convenient, adapts to more legacy system environments, expands application scenarios for developers, and while improving performance, also increases system compatibility, making .NET application development and deployment more flexible and extensive, providing users with a better experience.
II. Experience
(I) Importance of Testing Strategy
From the very beginning of a project, it is important to develop the habit of performing AOT publishing tests promptly whenever new functionality is added or newer syntax is used. Otherwise, issues accumulate and become extremely difficult to resolve later on. The webmaster paid a heavy price for neglecting this early on. The workaround was to recreate the project and then restore functionality one by one while performing AOT testing. After a week of overtime AOT testing, each AOT publishing process roughly went as follows:
- Internal network AOT publishing took 2-3 minutes each time, during which I could only read requirements documents, technical articles, requirements documents, technical articles...
- After publishing, the program showed no effect. Double-clicking did not bring up the interface, and the process list did not contain it, indicating the program crashed. Check the system application event logs, which usually contain exception warnings.
- Based on the log information, check the code and modify the relevant APIs.
- Perform AOT publishing again, repeating steps 1-3.
After a week of effort, the project's AOT functionality tests finally passed normally, and that wrapped up the work.
(II) Points to Note for AOT and Solutions
1. Add rd.xml
Create an XML file in the main project, e.g., Roots.xml, with content similar to the following:
<linker>
<assembly fullname="CodeWF.Toolbox.Desktop" preserve="All" />
</linker>
For each project that needs AOT support, add an assembly node in this XML, where fullname is the assembly name. CodeWF.Toolbox.Desktop is the main project name of the webmaster's tool. Click here to view the source code.
Add an ItemGroup node in the main project to reference this XML file:
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
2. Prism Support
The webmaster uses the Prism framework and the DryIOC container. To support AOT, add the following NuGet packages:
<PackageReference Include="Prism.Avalonia" Version="8.1.97.11073" />
<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11073" />
Add the following to rd.xml:
<assembly fullname="Prism" preserve="All" />
<assembly fullname="DryIoc" preserve="All" />
<assembly fullname="Prism.Avalonia" preserve="All" />
<assembly fullname="Prism.DryIoc.Avalonia" preserve="All" />
Version 8.1.97.11073 is the last open-source version; 9.X and later are paid versions.
3. Reading and Writing App.config
In .NET Core, use the System.Configuration.ConfigurationManager package to operate App.config files. Add the following to rd.xml:
<assembly fullname="System.Configuration.ConfigurationManager" preserve="All" />
Using Assembly.GetEntryAssembly().location fails. Currently, the application configuration is obtained using ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None). Specifying a path will be researched later.
4. Using HttpClient
Add the following to rd.xml:
<assembly fullname="System.Net.Http" preserve="All" />
Generally, you may not use HttpClient directly. You can try third-party libraries like Refit for more convenient HTTP requests:
<assembly fullname="Refit" preserve="All" />
5. Dapper Support
Dapper AOT support requires installing the Dapper.AOT package. Add the following to rd.xml:
<assembly fullname="Dapper" preserve="All" />
<assembly fullname="Dapper.AOT" preserve="All" />
Database operation methods need to add the DapperAOT attribute, for example:
[DapperAot]
public static bool EnsureTableIsCreated()
{
try
{
using var connection = new SqliteConnection(DBConst.DBConnectionString);
connection.Open();
const string sql = $@"
CREATE TABLE IF NOT EXISTS {nameof(JsonPrettifyEntity)}(
{nameof(JsonPrettifyEntity.IsSortKey)} Bool,
{nameof(JsonPrettifyEntity.IndentSize)} INTEGER
)";
using var command = new SqliteCommand(sql, connection);
return command.ExecuteNonQuery() > 0;
}
catch (Exception ex)
{
return false;
}
}
6. System.Text.Json
Refer to JsonExtensions.cs
Serialization
public static bool ToJson<T>(this T obj, JsonSerializerOptions? options, out string? json, out string? errorMsg)
{
if (obj == null)
{
json = default;
errorMsg = "Please provide object";
return false;
}
if (options == null)
{
options = new JsonSerializerOptions()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
}
try
{
json = JsonSerializer.Serialize(obj, options);
errorMsg = default;
return true;
}
catch (Exception ex)
{
json = default;
errorMsg = ex.Message;
return false;
}
}
Deserialization
public static bool FromJson<T>(this string? json, JsonSerializerOptions? options, out T? obj, out string? errorMsg)
{
if (string.IsNullOrWhiteSpace(json))
{
obj = default;
errorMsg = "Please provide json string";
return false;
}
try
{
if (options == null)
{
options = new JsonSerializerOptions()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
Converters = { new NullableDateTimeConverter() }
};
}
obj = JsonSerializer.Deserialize<T>(json!, options);
errorMsg = default;
return true;
}
catch (Exception ex)
{
obj = default;
errorMsg = ex.Message;
return false;
}
}
The above configuration works normally for simple class serialization and deserialization. If the class contains nullable types like int?, double?:
public class Project
{
public int? Id { get; set; }
public string Name { get; set; }
public int Record { get; set; }
[XmlArray(ElementName = "Members")]
[XmlArrayItem(typeof(Member))]
public List<Member> Members { get; set; }
}
Then create a subclass of JsonSerializerContext to solve it:
[JsonSerializable(typeof(Project))]
[JsonSerializable(typeof(List<Member>))]
internal partial class ProjectJsonSerializerContext : JsonSerializerContext
{
}
Note: Just create it; no need to call it explicitly.
The traditional
JsonSerializer.Serialize(project)relies on runtime reflection to parse information like properties and attributes of theProjectclass, but AOT compilation will trim reflection metadata that is not explicitly referenced at compile time, causing serialization to fail (throwingNotSupportedExceptionor failing to serialize/deserialize certain properties).
ProjectJsonSerializerContextworks by generating static metadata at compile time.
7. Reflection Issues
Refer to the project CodeWF.NetWeaver
- Creating instances of
List<T>orDictionary<T>with specified types:
public static object CreateInstance(Type type)
{
var itemTypes = type.GetGenericArguments();
if (typeof(IList).IsAssignableFrom(type))
{
var lstType = typeof(List<>);
var genericType = lstType.MakeGenericType(itemTypes.First());
return Activator.CreateInstance(genericType)!;
}
else
{
var dictType = typeof(Dictionary<,>);
var genericType = dictType.MakeGenericType(itemTypes.First(), itemTypes[1]);
return Activator.CreateInstance(genericType)!;
}
}
- Failing to add elements by reflectively calling the
Addmethod ofList<T>andDictionary<T>. Below is pseudocode:
// List<T>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ child })
// Dictionary<Key, Value>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ key, value })
Solution: Convert to the implemented interface call:
// List<T>
(obj as IList).Add(child);
// Dictionary<Key, Value>
(obj as IDictionary)[key] = value;
- Getting the element count of arrays,
List<T>,Dictionary<key, value>
Similarly, reflectively getting the Length or Count property using reflection returns 0. The encapsulated Property method works correctly in non-AOT scenarios:
public static T Property<T>(this object obj, string propertyName, T defaultValue = default)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(nameof(propertyName));
var propertyInfo = obj.GetType().GetProperty(propertyName);
if (propertyInfo == null)
{
return defaultValue;
}
var value = propertyInfo.GetValue(obj);
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch (InvalidCastException)
{
return defaultValue;
}
}
AOT success: directly access the property by casting to the base type or implemented interface:
// Array
var length = ((Array)value).Length;
// List<T>
if (value is IList list)
{
var count = list.Count;
}
// Dictionary<key, value>
if (value is IDictionary dictionary)
{
var count = dictionary.Count;
}
8. Windows 7 Support
If the AOT output cannot run on Windows 7, add the YY-Thunks package:
<PackageReference Include="YY-Thunks" Version="1.1.4-Beta3" />
And specify the target framework as net9.0-windows.
9. WinForms / XP Compatibility
If it still doesn't run after step 8, please refer to the previous article ".NET 9 AOT Breakthrough - Supports Legacy Win7 and XP Environments - Code Workshop (dotnet9.com)" to add the VC-LTL package. I won't repeat it here.
10. Others
There are many other points to note. I will gradually perfect this article as I recall them.
III. Conclusion
Although AOT publishing tests may encounter many issues along the way, with timely testing and correct configuration adjustments, the project can eventually be published smoothly. I hope the summarized experience above will be helpful for everyone when using AOT, allowing you to avoid detours in the development process and improve project development efficiency and quality. At the same time, I look forward to everyone continuing to explore and summarize in practice, jointly promoting technological progress and development.
AOT reference projects:
- CodeWF.NetWeaver: https://github.com/dotnet9/CodeWF.NetWeaver
- CodeWF.Tools: https://github.com/dotnet9/CodeWF.Tools
- CodeWF.Toolbox: https://github.com/dotnet9/CodeWF.Toolbox