AOT Publishing Guide
AOT Publishing Guide
What is .NET AOT
.NET AOT (Ahead-of-Time) compilation compiles .NET applications directly into native code at build time. Unlike traditional JIT (Just-in-Time) compilation, AOT converts IL code into platform-specific native machine code during publishing.
Benefits of AOT
| Benefit | Description |
|---|---|
| Fast Startup | No JIT compilation required; significantly reduces startup time |
| Low Memory | Removes JIT compiler and IL code, reducing memory footprint |
| Small Deployment | Contains only necessary runtime code; generates a single executable |
| No Runtime Required | Target machine does not require .NET Runtime installation |
| Better Security | Native code is more difficult to reverse engineer than IL |
How WebApiClientCore Supports AOT
Traditional WebApiClientCore relies on runtime reflection to create interface proxy classes, which is not feasible in AOT environments because:
- Trimming - AOT publishing trims unused code; reflection-related type information may be lost
- No JIT - The runtime cannot dynamically generate proxy class code
To address these issues, WebApiClientCore provides Source Generator support to generate proxy class code at compile time.
Architecture Comparison
Traditional Mode:
┌─────────────────┐ Reflection ┌──────────────────┐
│ IHttpApi │ ──────────→ │ Runtime Proxy │
│ Interface │ │ Class │
└─────────────────┘ └──────────────────┘
AOT Mode:
┌─────────────────┐ Source ┌──────────────────┐
│ IHttpApi │ ──────────→ │ Compile-time │
│ Interface │ Generator │ Proxy Class │
└─────────────────┘ └──────────────────┘Source Generator Mechanism
Proxy Class Generation
The WebApiClientCore.Analyzers package contains a Source Generator that:
- Scans Interfaces - Finds all interfaces inheriting from
IHttpApi - Generates Proxy Classes - Creates an implementation class for each interface
- Registers Initializers - Uses
[ModuleInitializer]to automatically register proxy class types
Generated Code Example
For the following interface:
public interface IUserApi : IHttpApi
{
[HttpGet("api/users/{id}")]
Task<User> GetAsync(string id);
}The Source Generator will produce code similar to:
// HttpApiProxyClass.IUserApi.g.cs
namespace WebApiClientCore
{
partial class HttpApiProxyClass
{
[HttpApiProxyClass(typeof(IUserApi))]
sealed partial class IUserApi : IUserApi
{
private readonly IHttpApiInterceptor _apiInterceptor;
private readonly ApiActionInvoker[] _actionInvokers;
public IUserApi(IHttpApiInterceptor apiInterceptor, ApiActionInvoker[] actionInvokers)
{
_apiInterceptor = apiInterceptor;
_actionInvokers = actionInvokers;
}
[HttpApiProxyMethod(0, "GetAsync", typeof(IUserApi))]
Task<User> IUserApi.GetAsync(string p0)
{
return (Task<User>)_apiInterceptor.Intercept(_actionInvokers[0], new object[] { p0 });
}
}
}
}ModuleInitializer Registration
The generated initialization code ensures proxy class types are preserved during AOT trimming:
// HttpApiProxyClass.g.cs
static partial class HttpApiProxyClass
{
[ModuleInitializer]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpApiProxyClass))]
public static void Initialize()
{
}
}Project Configuration Steps
Prerequisites
[!important] .NET Version Requirements
- AOT Publishing: Requires .NET 8.0 or later
- JSON Source Generator:
PrependJsonSerializerContextmethod only supports .NET 8.0+- Source Generator: Supports .NET Standard 2.1+ and .NET 5.0+
1. Add NuGet Package References
<ItemGroup>
<!-- WebApiClientCore core package -->
<PackageReference Include="WebApiClientCore" Version="3.0.0" />
<!-- Source Generator package (must be referenced as Analyzer) -->
<PackageReference Include="WebApiClientCore.Analyzers" Version="3.0.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>2. Configure AOT Publishing Properties
Edit your .csproj file:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<!-- Enable AOT publishing -->
<PublishAot>true</PublishAot>
<!-- Enable trimming (implicitly enabled by AOT, but explicit is clearer) -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Optional: Invariant globalization mode (reduces size) -->
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Optional: Enable single file publishing -->
<!-- <PublishSingleFile>true</PublishSingleFile> -->
</PropertyGroup>3. Complete Project File Example
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WebApiClientCore" Version="3.0.0" />
<PackageReference Include="WebApiClientCore.Analyzers" Version="3.0.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>JSON Source Generator Configuration
In AOT environments, System.Text.Json also requires using source generators to avoid reflection.
Create JsonSerializerContext Derived Class
using System.Text.Json.Serialization;
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(User[]))]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(Order[]))]
// Add all JSON model types used in interfaces
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}Important Notes
- Must declare all JSON data types used in interfaces
- For collection types, declare both element type and collection type separately
- For generic types, declare each concrete generic parameter separately
// Example: Complete JSON type declarations
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(User[]))]
[JsonSerializable(typeof(List<User>))]
[JsonSerializable(typeof(ApiResponse<User>))]
[JsonSerializable(typeof(ApiResponse<List<User>>))]
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}WebApiClientCore Configuration
Register JSON Source Generation Context
Register JsonSerializerContext in dependency injection configuration:
using Microsoft.Extensions.DependencyInjection;
services
.AddWebApiClient()
.ConfigureHttpApi(options =>
{
// Register JSON source generation context
options.PrependJsonSerializerContext(AppJsonSerializerContext.Default);
});Complete Configuration Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
class Program
{
static void Main(string[] args)
{
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// Configure WebApiClientCore
services
.AddWebApiClient()
.ConfigureHttpApi(options =>
{
options.PrependJsonSerializerContext(AppJsonSerializerContext.Default);
});
// Register HTTP API interface
services.AddHttpApi<IUserApi>();
// Register background service
services.AddHostedService<AppService>();
})
.Build()
.Run();
}
}Publishing Commands and Options
Basic Publishing Command
# Publish as AOT application for current platform
dotnet publish -c Release
# Specify target platform
dotnet publish -c Release -r win-x64
dotnet publish -c Release -r linux-x64
dotnet publish -c Release -r osx-x64
dotnet publish -c Release -r osx-arm64Common Publishing Options
# Complete publishing command example
dotnet publish -c Release -r linux-x64 \
-p:PublishAot=true \
-p:PublishTrimmed=true \
-p:InvariantGlobalization=true \
-p:StripSymbols=true \
-p:OptimizationPreference=SpeedPublishing Options Explanation
| Option | Description |
|---|---|
-r <RID> | Target Runtime Identifier |
-p:PublishAot=true | Enable AOT publishing |
-p:PublishTrimmed=true | Enable trimming |
-p:InvariantGlobalization=true | Use invariant globalization mode |
-p:StripSymbols=true | Strip debug symbols (reduces size) |
-p:OptimizationPreference=Speed | Optimize for speed (or Size) |
-p:IlcOptimizationPreference=Speed | ILC compiler optimization preference |
Viewing Generated Files
# Publishing output directory
bin/Release/net8.0/<RID>/publish/
# Main files:
# - <app_name> (executable)
# - <app_name>.pdb (debug symbols, if not stripped)Complete Code Example
Project Structure
AppAot/
├── AppAot.csproj
├── Program.cs
├── AppJsonSerializerContext.cs
├── IUserApi.cs
├── User.cs
└── AppService.csInterface Definition
// IUserApi.cs
using WebApiClientCore.Attributes;
[LoggingFilter]
[HttpHost("https://api.example.com")]
public interface IUserApi
{
[HttpGet("api/users/{id}")]
Task<User> GetUserAsync(string id);
[HttpPost("api/users")]
Task<User> CreateUserAsync([JsonContent] User user);
[HttpGet("api/users")]
Task<User[]> ListUsersAsync();
}Data Model
// User.cs
using System.Text.Json.Serialization;
public class User
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("email")]
public string? Email { get; set; }
}JSON Source Generator
// AppJsonSerializerContext.cs
using System.Text.Json.Serialization;
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(User[]))]
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}Main Program
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
class Program
{
static void Main(string[] args)
{
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services
.AddWebApiClient()
.ConfigureHttpApi(options =>
{
options.PrependJsonSerializerContext(AppJsonSerializerContext.Default);
});
services.AddHttpApi<IUserApi>();
services.AddHostedService<AppService>();
})
.Build()
.Run();
}
}Background Service
// AppService.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class AppService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<AppService> _logger;
public AppService(IServiceScopeFactory scopeFactory, ILogger<AppService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _scopeFactory.CreateScope();
var userApi = scope.ServiceProvider.GetRequiredService<IUserApi>();
try
{
var users = await userApi.ListUsersAsync(stoppingToken);
_logger.LogInformation("Retrieved {Count} users", users.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "API call failed");
}
}
}Common Issues and Solutions
Issue 1: Proxy Class Not Found
Error: Cannot find proxy class for interface XXX
Cause: Source Generator not configured correctly or did not execute
Solution:
- Ensure
WebApiClientCore.Analyzersis referenced as Analyzer - Check
.csprojhasOutputItemType="Analyzer" ReferenceOutputAssembly="false" - Clean and rebuild:
dotnet clean && dotnet build - Check compilation output for generated
HttpApiProxyClass.*.g.csfiles
Issue 2: JSON Serialization Fails
Error: JsonSerializerContext has not registered type XXX
Cause: Missing type declaration in JsonSerializerContext
Solution:
- Add missing type declarations in
AppJsonSerializerContext - Note that collection types need separate declarations
- Generic types need declarations for each concrete parameter
// Wrong: Only declared User
[JsonSerializable(typeof(User))]
// Correct: Also declare collection types
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(User[]))]
[JsonSerializable(typeof(List<User>))]Issue 3: AOT Trimming Warnings
Error: ILTrim warnings or ILC warnings
Cause: Code uses patterns that are incompatible with AOT
Solution:
- Use
[DynamicallyAccessedMembers]attribute to annotate types accessed via reflection - Check for dynamic code generation usage (e.g.,
System.Reflection.Emit) - Use
[UnconditionalSuppressMessage]to suppress known-safe warnings
Issue 4: Types Being Trimmed
Error: Types or members not found at runtime
Cause: The AOT trimmer removes types deemed unused
Solution:
- Use
[DynamicDependency]to preserve dependencies - Use
[DynamicallyAccessedMembers]to annotate members to preserve - Configure trimming options in project file:
<ItemGroup>
<!-- Preserve framework libraries during trimming -->
<TrimmerRootAssembly Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>Issue 5: Large Publishing Size
Cause: Unnecessary dependencies or debug information are included
Solution:
- Enable
InvariantGlobalizationto reduce globalization data - Enable
StripSymbolsto strip debug symbols - Use
OptimizationPreference=Sizefor size optimization - Review and remove unnecessary NuGet packages
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
<StripSymbols>true</StripSymbols>
<OptimizationPreference>Size</OptimizationPreference>
</PropertyGroup>AOT Limitations and Considerations
Feature Limitations
| Limitation | Description | Alternative |
|---|---|---|
| No JIT Compilation | Runtime cannot generate new code | Use Source Generators |
| No Dynamic Loading | Cannot load external assemblies | Statically reference all dependencies |
| Limited Reflection | Some reflection operations are restricted | Use source generation or annotations |
| No COM Interop | Some COM scenarios not supported | Use P/Invoke instead |
| Cross-platform | Requires separate compilation per platform | CI/CD multi-target publishing |
WebApiClientCore Specific Limitations
- No Runtime Dynamic Interfaces - All
IHttpApiinterfaces must be defined at compile time - No Dynamic Attribute Modification - Attribute configuration must be finalized at compile time
- JSON Serialization Requires Source Generation - Must use the
System.Text.Jsonsource generator - No Newtonsoft.Json Support -
WebApiClientCore.Extensions.NewtonsoftJsonis incompatible with AOT
Best Practices
Development Phase Testing
- Test application functionality with
dotnet run - Verify all API calls work correctly before AOT publishing
- Test application functionality with
Pre-publish Checks
- Check compilation warnings, especially ILTrim/ILC warnings
- Test published application functionality
- Verify JSON serialization/deserialization
Version Management
- Keep
WebApiClientCoreandWebApiClientCore.Analyzersversions consistent - Version incompatibility will cause proxy class generation to fail
- Keep
Debugging Tips
- Use
<PublishAot>false</PublishAot>to temporarily disable AOT for debugging - Check the generated code in the
obj/Release/netX.X/generated/directory
- Use