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
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
.NET Version Requirements
- AOT Publishing: Requires .NET 8.0 or later (WebApiClientCore enables
IsAotCompatibleon thenet8.0andnet10.0targets) - 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="2.1.6" />
<!-- Source Generator package (must be referenced as Analyzer) -->
<PackageReference Include="WebApiClientCore.Analyzers" Version="2.1.6" 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="2.1.6" />
<PackageReference Include="WebApiClientCore.Analyzers" Version="2.1.6" 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