Native AOT Warnings: How to Fix IL2026, IL3050, IL2057 and IL2075
Author: Oğuzhan KARAGÜZEL ·

On This Page
In JIT vs AOT I compiled a small MVC application with Native AOT. The build finished successfully with a pile of warnings, but the application crashed with a TypeLoadException before it could answer a single request. My conclusion there was: don't ship a project that produces AOT warnings.
In this article I go one step further and take the four most common warnings one by one. For each I wrote a small example, compiled it with Native AOT and ran it. I show what the warning says, what happens at run time and how to fix it, with real output.
Environment: .NET 10, dotnet publish -c Release -r linux-x64 in a Linux container, <PublishAot>true</PublishAot> in the project file.
A quick reminder: why do the warnings appear?
While compiling, Native AOT analyses which code is used and puts only that in the output. Types, methods and properties it thinks are unused are left out. This is called trimming.
The trouble starts with reflection. In a call like Type.GetType("EmailNotifier"), which type is requested is only known at run time. The compiler can't see it in advance. Where it can't see, it warns us.
The warnings come in two families:
- IL2xxx: Trimming warnings. "The code needed here may not be in the compiled output."
- IL3xxx: AOT warnings. "Code may need to be generated at run time here, which isn't possible under AOT."
IL2026 and IL3050: JSON serialization
This is the pair you'll see most. A line as ordinary as this:
var todo = new Todo(1, "Finish the article", false);
Console.WriteLine(JsonSerializer.Serialize(todo));produces two warnings at build time:
warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)'
which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.
warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)'
which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling.And at run time:
InvalidOperationException: Reflection-based serialization has been disabled for this application.
Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.System.Text.Json normally discovers a type's properties at run time through reflection. Under Native AOT that path is closed.
Fix: the source generator. You declare the types you'll serialize on a JsonSerializerContext, and the compiler generates the code at build time:
[JsonSerializable(typeof(Todo))]
internal partial class AppJsonContext : JsonSerializerContext;
Console.WriteLine(JsonSerializer.Serialize(todo, AppJsonContext.Default.Todo));{"Id":1,"Title":"Finish the article","Done":false}The warnings are gone and the output is back. In an ASP.NET Core minimal API you wire the same context up like this:
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));IL2057: a type given by name
Code often written for a plugin or strategy setup:
var typeName = Environment.GetEnvironmentVariable("PLUGIN")!;
var type = Type.GetType(typeName);
var instance = Activator.CreateInstance(type!);The warning:
warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'.
It's not possible to guarantee the availability of the target type.I ran it two ways. When I passed EmailNotifier, a type in the same project, the type was found, but:
MissingMethodException: No parameterless constructor defined for type 'EmailNotifier'.The type was in the output but its constructor wasn't. Since the compiler never saw new EmailNotifier() anywhere in the code, it considered it unnecessary and dropped it.
When I passed a type from another library, the result was quieter: Type.GetType returned null. The type wasn't there at all.
Fix: use a registry instead of taking the type name as a string:
public static class Notifiers
{
static readonly Dictionary<string, Func<INotifier>> Registry = new()
{
["email"] = () => new EmailNotifier(),
["sms"] = () => new SmsNotifier(),
};
public static INotifier Create(string key) =>
Registry.TryGetValue(key, out var create)
? create()
: throw new ArgumentException($"Unknown notifier: {key}");
}Now every type is created explicitly with new in the code. The compiler sees them all and drops none. If an unknown key comes in, I get a clear error. The code is a bit longer, but the types that can be created now live in one place. I think that's a better design even without AOT.
IL2075: reading a type's properties
A small helper that turns an object into text:
static string Describe(object value)
{
var type = value.GetType();
return string.Join(", ", type.GetProperties().Select(p => $"{p.Name}={p.GetValue(value)}"));
}The warning:
warning IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties'
in call to 'System.Type.GetProperties()'. The return value of method 'System.Object.GetType()'
does not have matching annotations.To me this is the most dangerous of the four, because it doesn't fail. In my first attempt Describe(new Todo(2, "Cover", true)) returned an empty string. No exception, no log. Just a wrong result.
value.GetType() doesn't tell the compiler which type it will return at run time. So the compiler doesn't know that Todo's properties will be accessed through reflection, and it drops their metadata.
Fix: tell the compiler what you need. That's what the DynamicallyAccessedMembers attribute is for:
static string Describe<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value)
{
return string.Join(", ", typeof(T).GetProperties().Select(p => $"{p.Name}={p.GetValue(value)}"));
}'Id=2, Title=Cover, Done=True'I made the method generic and noted on T that "this type's public properties are needed". Now when Describe(todo) is called, the compiler keeps Todo's properties.
Something else interesting happened in this experiment. After I added the fixed method to the same project, the old Describe started returning the right result too. The reason is that the new method now keeps Todo's properties. The old method's warning was still there, though. So code that produces a warning can work today by accident and break tomorrow after an unrelated change. Don't dismiss a warning with "I tested it, it works".
When is it right to suppress a warning?
Sometimes you're sure the reflection code is safe. For example, the type it needs is already used explicitly elsewhere. Then you can silence the warning with UnconditionalSuppressMessage:
[UnconditionalSuppressMessage("Trimming", "IL2075",
Justification = "Only called with Todo; Todo's properties are kept by DescribeFixed.")]But I do this very rarely. As the example above shows, "it works" and "it will always work" aren't the same thing. I also write clearly in Justification why it's safe, so that when someone changes that code one day they can see what I assumed.
Warnings from libraries
When the warning comes from a package you use rather than your own code, it looks like this:
warning IL3053: Assembly 'Microsoft.EntityFrameworkCore' produced AOT analysis warnings.You can't fix this in your own code. Your options are:
- Check whether the package has an AOT-compatible version, or whether its page or docs say "AOT compatible".
- Use another AOT-compatible package that does the same job. For example source-generated
System.Text.Jsoninstead of Newtonsoft.Json. - Compile that project with ReadyToRun instead of Native AOT. That's the path I took for Elevare, the platform behind this site, because of MVC and EF Core.
If you're writing your own library
To see whether your library is AOT compatible, add this to the project file:
<IsAotCompatible>true</IsAotCompatible>This runs the AOT analyzers on the library's own build too. You see the warnings before someone using your library runs into them.
Summary
| Warning | What it says | What happened for me | Fix |
|---|---|---|---|
| IL2026 / IL3050 | JSON through reflection | InvalidOperationException | JsonSerializerContext |
| IL2057 | Type name as a string | Missing constructor or null type | Registry, explicit new |
| IL2075 | Reflection over properties | No error, empty result | DynamicallyAccessedMembers |
| IL3053 | The package itself is incompatible | The app crashed at startup | Compatible package or ReadyToRun |
Conclusion
Native AOT warnings don't say "this might be a problem"; they say "the compiler can't see this". In my experiments, one of the four warnings showed up as an exception, one as a missing constructor, one as null and one as an empty result with no error at all. The last one was the most dangerous. That's why my goal in projects I publish with AOT is zero warnings. If I can't fix a warning, I either change that code or don't use Native AOT for that project.
