Make Blazor session value serialization Native AOT and trimming compatible

Open
#70,430 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
25/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
csharp
Domain
backend

Research direction

The proposal identifies shared session and TempData value codecs, generated JSON metadata, enum descriptors, and [SupplyParameterFromSession] as the affected areas, but names no implementation files or tests. Start by locating the existing session and TempData serialization paths and their tests, then trace reflection-based type selection and enum handling. Done means fixed supported values and Int32-backed enums work with Native AOT and full trimming while preserving stored-data compatibility and session behavior.

Written by the indexing model from the issue text.

Description

area-blazor NativeAOT

Summary

This document proposes making existing Blazor session-value 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 framework descriptors 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 restoration already exist; the change replaces their reflection-dependent execution paths while preserving the supported value set, existing stored-data compatibility, and [SupplyParameterFromSession] behavior. Session and TempData share the value codec and enum contract, but keep their own envelope, restoration, and error semantics.

Goals

  1. Make fixed-type execution self-contained. The framework supplies generated JSON metadata and fixed dispatch for its supported built-in scalar and collection types. Single-value JSON parsing does not require reflection-based contract discovery, and applications need no registration for these fixed types.

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

  3. Preserve session behavior and stored data. Supported values retain their types and meanings, existing valid entries remain readable for available supported type identities, and writes retain the compatible representation. Key selection, null removal, persistence timing, request availability, and component delivery are unchanged.

  4. Make missing support diagnosable without disrupting ordinary upgrades. Supported ordinary applications require no new configuration or warnings solely because of this feature. With reflection fallback disabled, missing required support fails explicitly rather than appearing to restore null; malformed or stale stored data retains its existing warning-and-null recovery.

Non-goals

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

  2. Replace the session payload layout, require a data migration, or guarantee compatibility across renamed types or changed assembly identities. Existing supported identity compatibility remains required; no new type-evolution contract is added.

  3. Implement runtime discovery or access of component properties marked [SupplyParameterFromSession], generate their getters/setters, or change assignment, callbacks, or subscriptions. Those belong to the separate component supplier item. Build-time identification of enums needing storage descriptors is included; property-access generation is not.

  4. Add HTTP session availability to WebAssembly, browser storage, interactive circuits without an active request, or unrelated runtimes. This is not ProtectedSessionStorage or circuit-state persistence.

  5. Migrate to a general serializer-provider contract, redesign host DI/options, integrate JS interop, or change TempData's lifecycle/provider semantics. Overlapping storage-codec work is shared, not a second implementation.

Proposed solution

Keep the existing single-value storage contract and replace 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 remains:

Category Supported values
Scalars int, bool, string, Guid, DateTime, and enums whose underlying type is int
Nullable values Nullable forms of supported value types; non-null boxed nullable values retain their 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 the same supported-value rules, including nested object[]
Null Null collection entries/elements where supported; a null component value removes its session entry

This does not add other numeric types, non-Int32-backed enums, arbitrary DTOs, other collection definitions, other dictionary key types, or arbitrary nested typed collections. A declared object or interface parameter is not a new supported stored value type: its actual non-null runtime value must still belong to the existing set.

An enum discriminator identifies an available enum descriptor, not a type to load by name. Framework-controlled shape selection distinguishes a scalar from its supported nullable/array/collection/dictionary form. For session, these are internal selection identities: existing stored tokens and envelopes remain compatible rather than being replaced with new discriminator/shape fields.

Enum-only descriptor and resolver contract

The public contract provides enum identity, not a configurable serializer. Session and TempData use a single shared CascadingEnumDescriptor/ICascadingEnumDescriptorResolver contract rather than consumer-specific APIs. The following are API-reference declarations with implementation bodies omitted; the signatures remain proposed rather than existing or API-approved:

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 for an enum or discriminator it does not handle. Lookup directions must agree, and conflicting or mismatched descriptors are configuration failures rather than order-dependent selections. Descriptor construction validates the existing Int32-backed restriction; the generic enum constraint alone does not restrict the backing type.

The codec validates the exact supported value shape before consulting the resolver. For example, List<WorkflowStage> selects framework list handling and resolves typeof(WorkflowStage), not an application-configurable list codec. A resolver cannot replace a fixed codec, admit unsupported collection subclasses, or serialize non-enum application models.

Resolver composition makes generated library/application support available before capability validation, restoration, or persistence, without runtime assembly scanning. Contributions remain isolated between mapped applications. This does not require adopting #70407, new endpoint metadata, or a new DI service/lifetime.

Internal generated context for the fixed types

The internal generated JSON context covers int, bool, string, Guid, and DateTime, plus their supported nullable/array/collection/dictionary combinations. Exact type lookup selects generated metadata on writing; supported declared target types or fixed stored-token lookup select it on reading. STJ receives generated metadata rather than a runtime Type with a default reflection resolver.

Single-value JSON parsing uses a framework DOM/metadata path without reflection-backed contract discovery. Null and recursive object[] handling remain explicit framework operations. They do not require a generated application serializer for object or object[].

The fixed context and value operations are shared with TempData. TempData's provider-level Dictionary<string, JsonElement> metadata is part of that shared work; session still reads individual entries rather than loading a TempData dictionary or acquiring its consume/keep semantics.

Generated descriptors for application enums

For an enum identified through a [SupplyParameterFromSession] property's type, generated code references a closed framework enum descriptor. Its internal implementation supplies the enum's existing supported shape family: nullable enum, arrays, and concrete List, HashSet, Collection, and string-keyed Dictionary shapes with enum or nullable-enum elements.

The closed descriptor makes those typed operations available without MakeGenericType, reflected collection construction, or STJ's default generic enum-converter factory. The framework writes JSON numbers and reconstructs the enum or concrete collection, preserving nulls and numeric values without discovering named enum fields.

Scalar conversion using Enum.ToObject(Type, int) is valid for an available enum type. Enum.GetUnderlyingType(Type) and an enum value's IConvertible.GetTypeCode() are metadata/value operations, not independently established AOT blockers. Only Int32-backed enums remain supported; these APIs do not justify adding other backing types.

Persist and restore a scalar with unchanged component code

A component uses the same property declaration and existing session middleware setup before and after the change:

@code {
    [SupplyParameterFromSession]
    public int VisitCount { get; set; }
}

With an active configured HTTP session, a stored value is restored and the component's value after rendering is persisted. The same stored value is available on later requests until overwritten, removed, or expired; reading it does not consume it.

Persist changes, nulls, and explicitly named values

The existing named-property syntax remains unchanged:

@code {
    [SupplyParameterFromSession(Name = "selected-values")]
    public List<int?>? SelectedValues { get; set; }
}

The property name or explicit Name continues selecting the key, with existing case normalization and duplicate-key validation. Persistence uses the component's latest value after initial and streaming rendering have finished.

Setting the component value to null removes its session entry; null collection elements remain null elements. Missing or empty stored entries retain existing null/no-value restoration. Later deliveries use the component's current value rather than re-reading session, and subscription cleanup/callback behavior is unchanged.

Use an enum declared on a session-backed property

An application or library declares an Int32-backed enum:

public enum WorkflowStage
{
    New,
    Active,
    Complete
}

Its component uses the existing session-backed property syntax:

@code {
    [SupplyParameterFromSession]
    public WorkflowStage? Stage { get; set; }
}

Generation supplies a closed CascadingEnumDescriptor<WorkflowStage> and a resolver mapping typeof(WorkflowStage) and its discriminator to that descriptor. The framework descriptor also supplies supported enum collection forms. Restoration returns the enum or concrete enum collection, not an integer substitute.

This identifies storage types at build time; it does not generate the property's getter/setter or change component parameter delivery. Existing entries for the same available enum identity remain readable.

Round-trip heterogeneous values through JSON elements

A component can store a heterogeneous array:

@code {
    [SupplyParameterFromSession]
    public object[]? RecentValues { get; set; }
}

For an array containing an int[], a string, an Int32-backed enum, null, and another object[], the framework parses JSON into JsonElement values, using array enumeration or an internal JsonElement[]. Each non-null entry's stored identity selects the fixed handler or available enum descriptor; nested object[] values recurse through the same path.

The result retains the original CLR element types, not JSON elements or inferred substitutes. JSON token kind alone cannot distinguish an enum from an integer, a Guid from a string, or int[] from List<int>. The DOM is an internal parsing representation, not a new supported application value or a reason to generate an application array serializer.

If an enum is reached only through object, object[], an interface, or direct shared-codec writes and generation cannot infer it, the application must be able to declare that enum explicitly for descriptor generation. This creates the same closed enum descriptor, not an arbitrary payload codec. Declaration syntax and generated-resolver activation remain API-design questions; ordinary reflection-compatible upgrades do not acquire a universal new declaration requirement.

Restore a typed property or a stored runtime type

For a supported concrete property such as List<int> or WorkflowStage?, the supplier passes the declared target to the serializer. The framework selects its available fixed/enum handler directly and reads the stored value without newly requiring the stored token to resolve.

For object, applicable interface/abstract targets, and recursively typed array entries, the framework instead resolves the stored identity against its fixed token set or the available enum descriptor family's supported shape identities. It does not load arbitrary types named by the data or construct generic types dynamically.

These are two selection paths into the same restricted codecs, not different supported type sets or a public custom-serializer hook.

Read existing data without migration

An application upgrades while retaining session entries written before this change. Its components use the same [SupplyParameterFromSession] declarations and read valid entries for the same available type identities without clearing session or rewriting data first.

Preserve the existing type/value envelope, fixed clean tokens, enum-containing assembly-qualified tokens, recursive object[] entries, and JSON value conventions. Framework-owned mappings recognize those tokens for available supported types and select the enum identity plus its framework-controlled shape without Type.GetType(string). Writers retain existing compatible spellings.

The discriminator/shape pair in the shared enum design need not become new session payload fields. Shared typed execution and consumer-specific identity/envelope handling preserve session compatibility without requiring TempData to provide the same cross-version guarantee. This does not add compatibility across type renames or changed assembly identities.

Reject unsupported values and report missing support

Unsupported declared concrete types remain rejected; a polymorphic declaration does not admit an unsupported runtime value. Unsupported runtime values and serialization failures continue surfacing during persistence.

Missing or empty stored data retains its existing null/no-value behavior. Malformed or stale stored data continues producing the existing restoration warning and null result; an obsolete stored token is not automatically evidence of missing application configuration.

When reflection fallback is disabled, missing required enum/type support or invalid descriptor configuration must fail explicitly and identify the affected support/operation. It must not be swallowed by the malformed-data recovery path, silently omit a value, or retry through reflection. Getter failures retain the supplier's existing logging and remain outside storage contract generation.

This session-specific strict failure decision does not change TempData's provider-level recovery. Shared codec changes must preserve its warning-and-empty load recovery, failed-cookie deletion, and surfaced save errors.

Retain request-specific session availability

With an HTTP request but no configured session, existing session-configuration failures remain explicit. Without the request context needed for session access, the supplier continues warning and skipping restoration/persistence rather than inventing browser or circuit session access.

Session middleware/configuration, cookie establishment, and storage expiration are not redesigned. These are preserved availability boundaries, not additional hosts to implement.

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.