Trimming and Native AOT
The .NET 10 build of the package is marked safe to trim and to compile ahead of time. Most methods use no
reflection at all. About twenty members do, and they are annotated, so the compiler warns you at the exact call
that can break. Read this page before you set PublishTrimmed or PublishAot in an app that uses the library.
At a glance
| Method | What it does |
|---|---|
ToJsonString, FromJson<T> | Reflection-based JSON. Warn with IL2026 and IL3050; throw at run time when reflection-based JSON is off |
ToSafeJson, ToCanonicalJson, ContentHash | Reflection-based JSON with fixed options. Warn with IL2026 and IL3050 |
DeepClone<T> | Copies through reflection-based JSON. Warns with IL2026 and IL3050 |
MapTo<TDestination>, UpdateFrom<T> | Copy properties between types by reflection. Warn with IL2026 and IL3050 |
SortBy (4 overloads) | Sorts by property names in a string. Warns with IL2026 and IL3050 |
CompareProperties<T>, ApplyTo | Diff and replay property values by reflection. Warn with IL2026 |
AsDictionary, ToQueryString, TrimStringProperties | Read or write every property of the runtime type. Warn with IL2026 |
GetPropertyValue, GetPropertyValue<T>, SetPropertyValue | Read or write one property by name. Warn with IL2026 |
FormatWith, WithQuery | Read placeholders or query values from an object's properties. Warn with IL2026 |
ToTable, ToMarkdownTable, ToHtmlTable, ToCsv | Reflect over typeof(T); T is annotated, so no warning and nothing is trimmed away |
ToObject<T> | Sets properties of T by name; T is annotated |
GetInterfacesAndAbstractClasses, IsAssignableToGenericType, GetGenericArgumentsOf | Type receiver annotated to keep interfaces |
GetDefaultValue, IsRecord | Type receiver annotated to keep the constructor or methods they read |
What IsAotCompatible means
The library project sets <IsAotCompatible>true</IsAotCompatible> for its net10.0 target. That one property
turns on the trim, AOT and single-file analyzers while the library is built, and marks the assembly as trimmable.
The build treats warnings as errors, so every reflection call inside the library is either annotated or proven
safe. For you this means three things:
- Methods without annotations are safe in a trimmed or Native AOT app. You get no warning and they behave the same.
- Methods with
[RequiresUnreferencedCode]or[RequiresDynamicCode]produce a warning at your call site. The warning is the signal to read the next sections. - Generic methods whose
Tcarries[DynamicallyAccessedMembers]tell the trimmer which members of your type to keep. They work after trimming without any warning, as long as the trimmer can seeT.
Order order = new Order("INV-001", 99.5m);
string json = order.ToJsonString(); // warnings IL2026 and IL3050 on this line
string csv = new[] { order }.ToCsv(); // no warning
bool paid = "Paid".EqualsIgnoreCase("paid"); // no warning
public sealed record Order(string Number, decimal Total);
warning IL2026: Using member 'System.ObjectExtensions.ToJsonString(Object, JsonSerializerOptions)' which has
'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This method uses
reflection-based System.Text.Json serialization, which trimming and ahead-of-time compilation can break.
Serialize with a source-generated JsonSerializerContext instead.
warning IL3050: Using member 'System.ObjectExtensions.ToJsonString(Object, JsonSerializerOptions)' which has
'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. ...
The library uses two warning messages. JSON members say:
This method uses reflection-based System.Text.Json serialization, which trimming and ahead-of-time compilation
can break. Serialize with a source-generated JsonSerializerContext instead.
Reflection members say:
This method reads the object's properties through reflection, which trimming can remove. Keep the type's public
properties with [DynamicallyAccessedMembers] or a TrimmerRootDescriptor.
The package ships netstandard2.1 and net10.0 builds. An app that targets .NET 10 gets the net10.0 build, the
one marked IsAotCompatible. An app on .NET 8 or .NET 9 gets the netstandard2.1 build. That build carries the
same annotations (the attributes are polyfilled), but it is not marked AOT-compatible and was not checked by the
analyzers. Target .NET 10 if you publish trimmed or with Native AOT.
Members that need reflection
These are all the public members with [RequiresUnreferencedCode] (warning IL2026) or [RequiresDynamicCode]
(warning IL3050). Everything else in the library has neither attribute.
Reflection-based JSON
These members call System.Text.Json without a source-generated context. All six carry both attributes.
| Member | IL2026 | IL3050 | Details |
|---|---|---|---|
ToJsonString(this object? obj, JsonSerializerOptions? options = null) | yes | yes | JSON |
FromJson<T>(this string json, JsonSerializerOptions? options = null) | yes | yes | JSON |
ToSafeJson(this object? obj, params string[] additionalSensitiveNames) | yes | yes | Safe logging |
ToCanonicalJson(this object? obj) | yes | yes | Canonical JSON |
ContentHash(this object? obj) | yes | yes | Content hash |
DeepClone<T>(this T source) | yes | yes | Compare, clone and map |
PrettyJson, MinifyJson and IsValidJson work on JSON text with JsonDocument. They carry no attributes and
are safe to use in a trimmed or AOT app.
Mapping between types
MapTo and UpdateFrom read and write properties by reflection and build generic collection types at run time.
Both carry both attributes.
| Member | IL2026 | IL3050 | Details |
|---|---|---|---|
MapTo<TDestination>(this object source) | yes | yes | MapTo |
UpdateFrom<T>(this T target, object source, bool ignoreNullValues = true) | yes | yes | UpdateFrom |
Sorting by name
SortBy resolves property names from a string such as "-total,customer.name" and builds OrderBy /
ThenBy calls at run time. All four overloads carry both attributes.
| Member | IL2026 | IL3050 |
|---|---|---|
SortBy<T>(this IEnumerable<T> source, string? sort) | yes | yes |
SortBy<T>(this IEnumerable<T> source, string? sort, params string[] allowedProperties) | yes | yes |
SortBy<T>(this IQueryable<T> source, string? sort) | yes | yes |
SortBy<T>(this IQueryable<T> source, string? sort, params string[] allowedProperties) | yes | yes |
See Paging and sorting for an AOT-safe alternative with a switch and
plain OrderBy calls.
Property access by reflection
These members read the properties of the object's runtime type. They carry only [RequiresUnreferencedCode]:
they need the properties to survive trimming, but generate no code at run time.
| Member | IL2026 | IL3050 | Details |
|---|---|---|---|
CompareProperties<T>(this T before, T after) | yes | no | CompareProperties |
ApplyTo(this IEnumerable<PropertyChange> changes, object target) | yes | no | ApplyTo |
AsDictionary(this object source, BindingFlags bindingAttr = ...) | yes | no | AsDictionary |
ToQueryString(this object source, string? collectionSeparator = null) | yes | no | ToQueryString |
TrimStringProperties(this object source) | yes | no | TrimStringProperties |
GetPropertyValue(this object target, string propertyName) | yes | no | GetPropertyValue |
GetPropertyValue<T>(this object target, string propertyName) | yes | no | GetPropertyValue |
SetPropertyValue(this object target, object? value, string propertyName) | yes | no | SetPropertyValue |
FormatWith(this string template, object source, IFormatProvider? provider = null) | yes | no | FormatWith |
WithQuery(this Uri uri, object parameters) | yes | no | WithQuery |
The attribute is on the method, so the warning appears even when you pass a dictionary to FormatWith or
WithQuery, or when the type's properties are already kept. The dictionary ToQueryString (on
IDictionary<string, object?>) is a different method and carries no attribute.
Generic members that keep their own properties
Some members only reflect over a type they can name at compile time: the T of the call, or the Type you pass.
Instead of a warning, they annotate that type parameter with [DynamicallyAccessedMembers]. The trimmer then keeps
the listed members of the type you use. You get no warning and no missing columns.
ToTable, ToMarkdownTable, ToHtmlTable and ToCsv
The four table writers read the public properties of typeof(T). T is annotated with
DynamicallyAccessedMemberTypes.PublicProperties, so the trimmer keeps every public property of T, even ones
your code never reads directly.
InvoiceLine[] lines = { new InvoiceLine("A-100", 2), new InvoiceLine("B-200", 5) };
string csv = lines.ToCsv();
// → "Sku,Quantity\r\nA-100,2\r\nB-200,5\r\n"
public sealed record InvoiceLine(string Sku, int Quantity);
These methods use typeof(T), not the runtime type of each item. A List<object> gives T = object, which has no
public properties, so the table has no columns. A List<Animal> that holds Dog items shows only the properties
of Animal. Pass a sequence typed as the class whose columns you want.
The output format of each writer is on Console output, including ToCsv.
ToObject
ToObject<T> creates a T and sets each public writable property whose name matches a dictionary key. T is
annotated with PublicProperties. That matters most here: a key whose property was trimmed away is silently
ignored, and the annotation is what prevents that.
IDictionary<string, object?> row = new Dictionary<string, object?>
{
["Plan"] = "team",
["Seats"] = "42",
["BillingDay"] = "Monday",
};
Subscription subscription = row.ToObject<Subscription>();
// subscription.Plan → "team"
// subscription.Seats → 42
// subscription.BillingDay → DayOfWeek.Monday
public sealed class Subscription
{
public string? Plan { get; set; }
public int Seats { get; set; }
public DayOfWeek BillingDay { get; set; }
}
See Dictionaries for conversion rules and exceptions.
Passing T through your own generic code
The annotation only works when the trimmer knows T. If you wrap one of these methods in your own generic method,
your T must carry the same annotation. Otherwise the trim analyzer reports IL2091 on the inner call.
using System.Diagnostics.CodeAnalysis;
string csv = ReportExporter.ExportCsv(new[] { new InvoiceLine("A-100", 2) });
// → "Sku,Quantity\r\nA-100,2\r\n"
public static class ReportExporter
{
// Without the attribute on T, the call to ToCsv reports warning IL2091.
public static string ExportCsv<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(
IEnumerable<T> rows)
{
return rows.ToCsv();
}
}
Type helpers
Five Type extensions annotate the Type receiver. Calling them on typeof(SomeType) is safe after trimming. If
you pass a Type from a variable or from GetType(), the analyzer can warn on your call, because it cannot see
which type it is.
| Member | Members kept |
|---|---|
GetInterfacesAndAbstractClasses | Interfaces |
IsAssignableToGenericType | Interfaces |
GetGenericArgumentsOf | Interfaces |
GetDefaultValue | PublicParameterlessConstructor |
IsRecord | PublicMethods, NonPublicMethods |
Type[]? itemTypes = typeof(List<int>).GetGenericArgumentsOf(typeof(IEnumerable<>));
// → [typeof(int)]
object? emptyId = typeof(Guid).GetDefaultValue();
// → Guid.Empty
bool isRecord = typeof(Order).IsRecord();
// → true
What happens at run time
A warning does not stop the build (unless your project treats warnings as errors). It tells you what can go wrong after publishing.
Reflection-based JSON is off
When the project sets PublishTrimmed or PublishAot, the .NET SDK sets
JsonSerializerIsReflectionEnabledByDefault to false. This applies to every build of the app, not only the
published output, so dotnet run already has reflection-based JSON off. System.Text.Json then refuses to
serialize with options that have no TypeInfoResolver. In that app:
ToJsonStringandFromJson<T>without options throwInvalidOperationException. The message says that reflection-based serialization has been disabled and asks for the source generator or aTypeInfoResolver.ToSafeJson,ToCanonicalJsonandContentHashalways use their own options, which have no resolver, so they throwInvalidOperationExceptiontoo. There is no way to pass a context to them.DeepClonebuilds its ownDefaultJsonTypeInfoResolver, so it does not hit that switch. It still depends on reflection over members the trimmer may have removed, so a copy can miss data. Do not rely on it in a trimmed app.
Order order = new Order("INV-001", 99.5m);
string json = order.ToJsonString();
// → throws InvalidOperationException (reflection-based serialization is disabled)
Unit tests run in a test project that sets neither PublishTrimmed nor PublishAot, so reflection-based JSON is
on there. Run the app itself, or set
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault> in the test project,
to see the failure before production does.
Reflection over trimmed properties
The reflection members in Property access by reflection do not throw because of trimming itself. They see fewer properties. The trimmer removes a property getter or setter that no code calls directly, so:
AsDictionary,ToQueryStringandComparePropertiesreturn fewer entries.TrimStringPropertiesskips properties it can no longer find.GetPropertyValueandSetPropertyValuethrowArgumentExceptionfor a name that no longer exists, the same exception as for a misspelled name.FormatWiththrowsFormatExceptionfor a placeholder whose property was removed.
MapTo, UpdateFrom and SortBy also carry [RequiresDynamicCode]. In a Native AOT app they can fail at run time
when they need a generic type or method that was not compiled ahead of time.
Use source-generated JSON instead
The JSON warnings go away when you call System.Text.Json with a source-generated JsonSerializerContext. The
generator writes the serialization code at compile time, so nothing needs reflection at run time.
Declare a context
List every type you serialize, including the collection types, on a partial class that derives from
JsonSerializerContext.
using System.Collections.Generic;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
Replace ToJsonString and FromJson
Call JsonSerializer with the generated JsonTypeInfo<T> for the type.
using System.Text.Json;
Order order = new Order("INV-001", 99.5m);
string json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// → "{\"Number\":\"INV-001\",\"Total\":99.5}"
Order? parsed = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// parsed → Order { Number = "INV-001", Total = 99.5 }
Replace DeepClone
Round-trip through the same context. For a record whose properties are all immutable values, order with { } is
already a full copy.
using System.Text.Json;
List<Order> orders = new List<Order> { new Order("INV-001", 99.5m), new Order("INV-002", 12m) };
byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(orders, AppJsonContext.Default.ListOrder);
// '!' is safe: the JSON of a non-null list is never the literal null.
List<Order> copy = JsonSerializer.Deserialize(bytes, AppJsonContext.Default.ListOrder)!;
// copy.Count → 2
// ReferenceEquals(copy, orders) → false
Passing a context through options
ToJsonString and FromJson<T> accept JsonSerializerOptions. If you pass options whose TypeInfoResolver is a
source-generated context, the call works at run time even with reflection-based JSON off. The warnings stay,
because the analyzer cannot see what you pass. Prefer the direct JsonSerializer calls above in new code.
using System.Text.Json;
JsonSerializerOptions options = AppJsonContext.Default.Options;
string json = order.ToJsonString(options);
// → "{\"Number\":\"INV-001\",\"Total\":99.5}"
Order? parsed = json.FromJson<Order>(options);
// parsed.Number → "INV-001"
ToJsonString serializes obj.GetType(), the runtime type. If that type is not listed in the context,
System.Text.Json throws NotSupportedException. Add a [JsonSerializable] line for each derived type you pass.
Keep a type's properties for reflection
If you must use a reflection member in a trimmed app, keep the properties it reads, then silence the warning at
the call with a justification. Mark the class itself with [DynamicallyAccessedMembers]: the trimmer then keeps
those members on the class and on classes derived from it.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
IDictionary<string, object?> fields = AuditWriter.ToFields(new AuditEntry { User = "ahmad", Action = "login" });
// fields["User"] → "ahmad"
// fields["Action"] → "login"
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public sealed class AuditEntry
{
public string User { get; set; } = string.Empty;
public string Action { get; set; } = string.Empty;
}
public static class AuditWriter
{
[UnconditionalSuppressMessage(
"Trimming",
"IL2026",
Justification = "AuditEntry keeps its public properties through [DynamicallyAccessedMembers].")]
public static IDictionary<string, object?> ToFields(AuditEntry entry)
{
return entry.AsDictionary();
}
}
A TrimmerRootDescriptor XML file in the app project does the same for types you cannot annotate, such as types
from another library.
Suppressing IL2026 keeps the build quiet but proves nothing. Do it only when the type's properties are kept, and
never suppress IL3050 on MapTo, UpdateFrom or SortBy in a Native AOT app without testing the published output.
See also
- JSON and safe logging:
ToJsonString,FromJson<T>,ToSafeJsonand the JSON text helpers. - Compare, clone and map:
DeepClone,CompareProperties,MapToandUpdateFrom. - Paging and sorting:
SortByand an AOT-safe way to sort by name. - Conventions: the rules every method in the library follows.