Make Blazor TempData value serialization Native AOT and trimming compatible

Open
#70,428 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
28/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
csharp
Domain
backend, devtools

Research direction

The issue proposes replacing TempData’s reflection dependent serialization paths, but does not identify implementation files or tests. Start by locating TempData value codecs and both built in providers, then read the related session serialization work in #70407. Done means fixed generated metadata and enum descriptors preserve supported TempData behavior under Native AOT and trimming, with missing support reported clearly.

Written by the indexing model from the issue text.

Description

area-blazor NativeAOT

Summary

This document proposes making existing Blazor TempData serialization compatible with Native AOT and full trimming through fixed internal codecs and enum-only descriptors. The framework uses an internal generated JSON context for the fixed supported types; generated application/library code resolves sealed CascadingEnumDescriptor<T> instances for application enums. The framework owns numeric enum encoding and supported collection handling, so the public interface cannot supply arbitrary serializers or admit non-enum payloads.

Serialization and value restoration already exist; the change replaces their reflection-dependent execution paths while preserving the supported value set and TempData semantics. Both built-in providers also use generated metadata for their stored dictionary. TempData is ephemeral and opt-in, so this proposal does not guarantee payload compatibility across versions.

Goals

  1. Make fixed-type execution self-contained. The framework supplies generated JSON metadata and explicit descriptors for its supported built-in scalar and collection types, plus the providers' dictionary envelope. Applications need no registration for these types, and execution does not invoke STJ's reflection-based contract discovery.

  2. Make application enum support explicit, generatable, and bounded. Generated resolvers supply available enum type identities through framework-controlled generic descriptors. The framework supplies numeric enum serialization and supported collection construction, including values reached through direct ITempData writes and heterogeneous arrays. The interface exposes no JSON metadata or custom serialization callbacks.

  3. Preserve TempData behavior. Reading, retaining, overwriting, deleting, and persisting values retain their current effects with either built-in provider. Serialization changes do not change HTTP availability, streaming restrictions, provider configuration, or component delivery.

  4. Make missing support diagnosable without disrupting ordinary upgrades. Supported untrimmed applications require no new configuration or warnings solely because of this feature. Restricted execution does not silently use reflection or lose a value without the existing failure reporting identifying the problem.

Non-goals

  1. Expand the supported value set, introduce arbitrary application-model storage, or let a general serializer provider override TempData's supported-type rules.

  2. Guarantee version-to-version payload compatibility or add migration machinery. TempData is ephemeral and opt-in; preserving payloads across application/framework upgrades is not a requirement. Compatibility with MVC TempData storage and changes to cookie protection, naming/chunking, or the server-session storage key are also outside this item.

  3. Implement runtime discovery or access of component properties marked [SupplyParameterFromTempData], generate their getters/setters, or change parameter assignment, callback lifecycle, or subscriptions. Those belong to the separate component supplier item. Build-time identification of the enum types needing storage descriptors is included here; generating component property access is not.

  4. Add TempData availability to WebAssembly, interactive circuits without an active request, or unrelated runtimes. Browser session storage and server-side HTTP session are not interchangeable.

  5. Migrate TempData to a general serializer-provider contract, redesign host DI/options, or integrate JS interop. This is a bounded change to TempData's JSON storage execution, not a requirement to adopt the general Blazor serializer.

Proposed solution

Keep TempData's dictionary/value codec and replace its dynamic serialization selection with fixed internal dispatch and enum descriptors. Framework codecs cover the finite non-enum type set. Generated resolvers identify application enums that cannot be known when the framework is compiled, without providing a general serialization extension point. Neither path requires runtime generic converter construction in restricted execution.

The supported value set is unchanged:

Category Supported values
Scalars int, bool, string, Guid, DateTime, and enums whose underlying type is int
Nullable values Nullable forms of the supported value types; a non-null boxed nullable value retains its existing underlying runtime representation
Arrays Single-dimensional zero-based arrays of supported scalar/nullable elements
Collections Exact List<T>, HashSet<T>, and Collection<T> shapes with supported scalar/nullable elements
Dictionaries Exact Dictionary<string, T> shapes with supported scalar/nullable values
Heterogeneous arrays object[], whose entries recursively follow these same supported-value rules, including nested object[]
Null Null entries and null elements where already supported

This does not admit arbitrary collection implementations, multidimensional arrays, general nested collections, non-string dictionary keys, non-Int32-backed enums, arbitrary objects, or additional numeric types.

An enum discriminator identifies a registered enum descriptor, not an arbitrary CLR type. The codec also records a framework-defined shape tag to distinguish an enum scalar from its supported array, collection, or dictionary shape. Neither discriminator nor shape tag requires assembly-qualified type-name lookup. There is no requirement to preserve the current payload layout or type-token spelling across versions.

Enum-only descriptor and resolver contract

The public contract provides enum identity, not a configurable serializer. Generated code constructs a framework-provided CascadingEnumDescriptor<T> and resolves it by enum type on writing or enum discriminator on reading. CascadingEnumDescriptor, CascadingEnumDescriptor<T>, and ICascadingEnumDescriptorResolver are shared with session value serialization rather than named for one storage consumer. The following are API-reference declarations with implementation bodies omitted; these are the selected proposal names, not existing or API-approved APIs:

public abstract class CascadingEnumDescriptor
{
    private protected CascadingEnumDescriptor();

    public Type EnumType { get; }

    public string Discriminator { get; }
}

public sealed class CascadingEnumDescriptor<T> : CascadingEnumDescriptor
    where T : struct, Enum
{
    public CascadingEnumDescriptor(string discriminator);
}

public interface ICascadingEnumDescriptorResolver
{
    CascadingEnumDescriptor? GetDescriptor(Type enumType);

    CascadingEnumDescriptor? GetDescriptor(string discriminator);
}

The generic constraint permits only non-nullable enum types as T. The generic class is sealed, and the base constructor prevents application-defined subclasses. There is no public JSON metadata, read/write delegate, converter, overridable serialization method, or descriptor for an arbitrary application type. Enum serialization and typed collection operations are internal framework implementations.

Generated application/library code implements the resolver; the enum itself does not. A resolver returns null when it does not handle an enum type or discriminator. Descriptors returned in either direction must agree on the enum type and discriminator. Conflicting descriptors are configuration errors, not order-dependent choices. Descriptor construction validates the existing Int32-backed enum restriction; the generic enum constraint alone does not impose an underlying integral type.

The codec validates the exact supported value shape before consulting this resolver. For example, List<SaveStatus> selects the framework's list shape and resolves the descriptor for typeof(SaveStatus), not a customizable descriptor for the whole list. The resolved descriptor's enum type must match. A resolver cannot replace a fixed-type codec, broaden the admitted shape set, or cause a non-enum model to be serialized.

Resolver composition must make generated library and application support available before the first load or save; it must not discover resolvers by scanning loaded assemblies at runtime. No adoption of the general Blazor serializer in #70407 is required. The session-value serialization item shares the value codec; overlapping work must be delivered once while respecting each consumer's own storage requirements and error behavior.

Internal generated context for the fixed types

The internal generated JSON context covers int, bool, string, Guid, and DateTime, their supported nullable/array/collection/dictionary combinations, and the providers' Dictionary<string, JsonElement> envelope. It retains the existing JSON value conventions. Null and recursive object[] handling remain explicit codec operations rather than an invitation to serialize arbitrary object values.

For non-enum entries outside the explicit null/object[] handling, an explicit switch or equivalent fixed lookup selects internal serialization metadata on writing. On reading, a fixed tag lookup selects the same internal metadata. STJ receives generated metadata rather than just a runtime Type and options with a default reflection resolver. Exact runtime-type matching preserves the current rejection of unsupported collection subclasses. This fixed-type metadata remains internal and is never returned by the enum resolver.

Both cookie-provider unprotection paths and the server-session provider use the generated envelope metadata. Fixing value descriptors alone would leave these provider-level default STJ calls unresolved.

Generated descriptors for application enums

For an enum identified through a [SupplyParameterFromTempData] property's type, generated code provides a resolver referencing a CascadingEnumDescriptor<T> for that concrete enum. The generic framework implementation supplies typed operations for the enum and its supported shape family: nullable enum, arrays, and the already-supported List, HashSet, Collection, and string-keyed Dictionary shapes with enum or nullable-enum elements.

Generated code statically instantiates the closed descriptor, so the associated typed operations are available without MakeGenericType, reflected collection construction, or a runtime STJ enum-converter factory. It does not generate arbitrary payload serializers or expose JSON contracts. The framework writes enum values directly as JSON numbers and reconstructs the enum or supported typed collection internally, preserving nulls and numeric values without requiring discovery of named enum fields.

Scalar enum conversion itself is not a new Native AOT problem: the existing non-generic Enum.ToObject(Type, int) works with an available enum type without discovering its named fields. Likewise, type lookup is not categorically unavailable under Native AOT; the current string from storage simply does not establish which types must be preserved. Explicit generated descriptors remove that dependency.

Only Int32-backed enums are admitted by this proposal, matching existing support. Knowing how to identify another underlying integral type does not itself expand TempData's accepted types.

Store and restore a simple value

An application uses the same ITempData API before and after the change:

tempData["notice"] = "Saved";

On the next request:

var notice = tempData.Get("notice");

notice is "Saved" in ordinary, fully trimmed, and Native AOT execution. Reading schedules the entry not to persist unless it is retained or written again; it does not immediately remove the value from the current request's dictionary.

Peek, keep, overwrite, and consume

The following existing operations retain their meaning:

var notice = tempData.Peek("notice");
tempData.Keep("notice");
tempData.Keep();
tempData["notice"] = "Updated";

Peek does not consume, Keep retains the applicable values, and assignment retains the new value. The indexer getter and successful dictionary TryGetValue consume like Get; enumeration retains its existing consumption behavior. Keys remain case-insensitive, and loading stays lazy.

Persistence writes only retained entries, including values supplied again by existing component persistence callbacks. An unused TempData instance does not cause a gratuitous storage rewrite. A null value written through the indexer remains a retained null entry rather than acquiring new remove-on-null semantics.

Round-trip nullable and collection values

Existing writes remain unchanged:

tempData["attempts"] = new List<int?> { 1, null, 3 };
tempData["labels"] = new Dictionary<string, string> { ["state"] = "saved" };
tempData["items"] = new object?[] { 42, "Saved", new int[] { 1, 2 }, null };

The next request restores the same supported concrete collection types and values, including null elements and recursively typed entries. Keeping and re-saving the restored values must preserve their types and values; byte-identical serialization is not a feature requirement.

Use an enum declared on a TempData-bound property

An application or library can declare an Int32-backed enum and use it in a TempData-bound property:

public enum SaveStatus
{
    Pending,
    Completed
}

[SupplyParameterFromTempData]
public SaveStatus? Status { get; set; }

Generation supplies a closed CascadingEnumDescriptor<SaveStatus> and a resolver that maps typeof(SaveStatus) and its discriminator to that descriptor. The framework descriptor supplies the supported shape family. This does not generate the property's getter/setter or change how the supplier delivers its value; those are separate runtime capabilities.

Write application enums directly through TempData

With the descriptor for SaveStatus available, the application can also write it and its supported shapes directly:

tempData["status"] = SaveStatus.Completed;
tempData["statuses"] = new List<SaveStatus> { SaveStatus.Completed };
tempData["optionalStatuses"] = new SaveStatus?[] { SaveStatus.Completed, null };

Restoration returns the enum or the corresponding enum collection, not an integer substitute. The same enum descriptor and framework shape handling apply if a value appears inside object[]; resolution does not depend on a receiving property being present.

If an enum is used only through direct ITempData writes, an object-typed property, or object[], the application must be able to explicitly declare it for descriptor generation. Generators cannot infer every possible runtime assignment. An explicit enum declaration supplies the same closed enum descriptor; it does not opt arbitrary model types into TempData. The declaration syntax and generated-resolver activation API remain API-design questions, not existing APIs.

At the component supplier boundary, preserve nullable target handling, already-compatible values, and the existing integer-to-enum conversion. Do not add string parsing, numeric coercion, enum validation, or new conversion rules. Do not require replacing Enum.ToObject unless validation establishes an actual compatibility problem with the supported types.

Use either existing built-in provider

The application continues selecting the cookie or server-session TempData provider through its existing configuration. Both support the same value contract; cookie protection, encoding/chunking, deletion of empty persisted data, and session storage behavior remain unchanged.

An active HTTP request is still required. Without one, the component supplier keeps its existing unavailable-value behavior and warning. Missing HTTP session configuration remains an error for the session-backed provider. Cookie persistence after the response has started retains the existing warning and restriction; this feature does not remove that restriction or introduce an implicit provider switch.

Reject unsupported values and report missing support

An application still cannot persist unsupported values:

tempData["unsupported"] = new object();

Unsupported writes retain their existing failure behavior. A supported application type whose required definition is missing in restricted execution is also not silently serialized through reflection, converted to a different type, or omitted; the failure must identify the missing type/support through the applicable error or existing provider warning.

Failed provider loads retain their current recovery behavior: the cookie provider logs a warning, deletes the failed cookie, and returns empty data; the session provider logs a warning and returns empty data. Failed whole-dictionary loads do not become partial successful loads. Save failures continue to surface. Missing-support diagnostics must remain visible within these existing boundaries rather than changing failure handling to satisfy this feature.

Dominant language
C#
Stars
38.5k
Forks
11.6k
Avg merge
3d 1h
Merged PRs (30d)
289

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/aspnetcore

All issues in dotnet/aspnetcore

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.