在ASP.NET Core 9 Web API中实现.NET 9 HybridCache遇序列化异常
.NET 9 HybridCache反序列化异常排查
问题现象
将ASP.NET Core Web API控制器代码迁移为使用.NET 9 HybridCache后,调用接口时抛出以下反序列化异常:
Exception caught in ExceptionHandlingMiddleware of localhost: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported.
相关代码
原控制器代码(正常运行)
[MapToApiVersion("1.0")] [HttpGet] [EnableQuery] public async Task<ActionResult<IEnumerable<Dto.Game>>> GetGameV1() { var games = await dbcontext.Game .Include(st => st.GameGameday) .AsNoTracking().ToListAsync(); if (games != null) { return mapper.Map<List<Dto.Game>>(games); } else { return NotFound(); } }
迁移后使用HybridCache的控制器代码(抛出异常)
[MapToApiVersion("1.0")] [HttpGet] [EnableQuery] public async Task<ActionResult<IEnumerable<Dto.Game>>> GetGameV1(CancellationToken ct = default) { var games = await cache.GetOrCreateAsync( $"games", // Unique key to the cache entry async cancel => { return await dbcontext.Game .Include(st => st.GameGameday) .AsNoTracking().ToListAsync(); }, cancellationToken: ct ); if (games != null) { return mapper.Map<List<Dto.Game>>(games); } else { return NotFound(); } }
实体类定义
public partial class Game { [JsonPropertyName("id")] public long Id { get; set; } [JsonPropertyName("gameday")] public GameDay? GameDay { get; set; } [JsonPropertyName("number")] public uint Number { get; set; } [JsonPropertyName("gameresults")] public List<GameResults>? GameResults { get; set; } public Game() {} }
public partial class GameResults { [Key] public long GameResultId { get; set; } public Game GameResultGame { get; set; } public uint GameResultRank { get; set; } public Player? GameResultPlayer { get; set; } } public partial class GameDay { [Key] public long GameDayId { get; set; } public DateOnly GameDayDate { get; set; } public string? GameDayLocation { get; set; } public List<Game>? GameDayGames { get; set; } }
HybridCache配置(Program.cs)
#pragma warning disable EXTEXP0018 builder.Services.AddHybridCache(options => { options.MaximumPayloadBytes = 1024 * 1024; options.MaximumKeyLength = 1024; options.DefaultEntryOptions = new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(60), LocalCacheExpiration = TimeSpan.FromMinutes(60) }; }); #pragma warning restore EXTEXP0018
异常原因
异常的核心原因是GameResults类不符合System.Text.Json(HybridCache默认序列化器)的反序列化要求:
GameResults中的GameResultGame是非可空引用类型,且未在类中显式定义无参构造函数,也未通过构造函数为该属性赋值;- 在C#非空引用类型规则下,编译器不会为包含未初始化非可空引用类型的类生成默认无参构造函数;
- HybridCache尝试反序列化缓存的
Game列表时,无法实例化关联的GameResults对象,因此抛出异常。
解决方案
以下三种方案可解决该问题,优先推荐前两种(符合C#代码规范):
方案1:给GameResults添加显式无参构造函数并初始化非可空属性
public partial class GameResults { [Key] public long GameResultId { get; set; } public Game GameResultGame { get; set; } public uint GameResultRank { get; set; } public Player? GameResultPlayer { get; set; } // 显式定义无参构造函数,初始化非可空属性 public GameResults() { GameResultGame = null!; // 使用null!抑制非空警告,或根据业务逻辑设置合理默认值 } }
方案2:将GameResults的非可空属性改为可空引用类型(业务允许的情况下)
如果业务逻辑允许GameResultGame为null,直接添加可空修饰符:
public Game? GameResultGame { get; set; }
修改后编译器会自动生成默认无参构造函数,System.Text.Json可正常反序列化。
方案3:自定义HybridCache的序列化选项
通过配置System.Text.Json的序列化规则,允许反序列化时处理非可空引用类型:
#pragma warning disable EXTEXP0018 builder.Services.AddHybridCache(options => { options.MaximumPayloadBytes = 1024 * 1024; options.MaximumKeyLength = 1024; options.DefaultEntryOptions = new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(60), LocalCacheExpiration = TimeSpan.FromMinutes(60) }; // 自定义序列化选项 options.SerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { static typeInfo => { if (typeInfo.Kind == JsonTypeInfoKind.Object) { // 遍历所有属性,将非可空引用类型标记为非必填 foreach (var property in typeInfo.Properties) { if (property.PropertyType.IsReferenceType && !property.PropertyType.IsNullable()) { property.IsRequired = false; } } } } } } }; }); #pragma warning restore EXTEXP0018
内容的提问来源于stack exchange,提问作者toby
相关产品推荐
相关产品推荐

