You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB自定义序列化器引发「Cannot create an abstract class」错误排查

问题分析与解决方案

咱们来一步步拆解你遇到的问题,找到根源并给出修复方案:

核心原因:缺失类型判别器导致驱动无法识别具体子类

当你注册自定义序列化器后,新版事件存入数据库时丢失了MongoDB驱动默认的类型判别器字段_t,这直接引发了两个关键问题:

  1. 读取时,驱动看到的文档没有_t标识,无法判断该实例化哪个FooBase的具体子类,只能尝试直接实例化抽象类FooBase,于是抛出「Cannot create an abstract class」错误。
  2. 因为驱动没识别出这是TypedFooBase<TestEvent>类型,自然不会触发你注册的BsonAncientEventSerializer<TestEvent>的反序列化方法。

而未注册序列化器时,驱动会自动给继承结构的类添加_t字段(存储具体子类的类型信息),读取时通过_t找到对应子类,所以能正常工作。你的自定义Serialize方法手动遍历字段和属性写入Bson文档,但完全没处理驱动默认的判别器逻辑,这就是问题的根源。


修复方案

方案1:在自定义序列化中手动添加判别器字段

修改BsonAncientEventSerializer<T>的Serialize方法,在写入文档开头时添加_t判别器:

public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TypedFooBase<T> value) 
{
    var bsonWriter = context.Writer;
    bsonWriter.WriteStartDocument();
    
    // 关键:写入MongoDB默认的类型判别器字段
    var discriminator = BsonSerializer.LookupDiscriminator(typeof(TypedFooBase<T>));
    bsonWriter.WriteName("_t");
    bsonWriter.WriteString(discriminator);
    
    // 你的原有字段/属性写入逻辑
    var fields = value.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public);
    var propsAll = value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
    var props = propsAll.Where(p => p.CanWrite).ToList();
    
    foreach (var field in fields) 
    {
        bsonWriter.WriteName(field.Name);
        BsonSerializer.Serialize(bsonWriter, field.FieldType, field.GetValue(value));
    }
    foreach (var prop in props) 
    {
        bsonWriter.WriteName(prop.Name);
        BsonSerializer.Serialize(bsonWriter, prop.PropertyType, prop.GetValue(value, null));
    }
    
    bsonWriter.WriteEndDocument();
}

这样存入的新版事件会包含_t字段,读取时驱动能识别到TypedFooBase<TestEvent>类型,进而触发你的自定义反序列化方法。

方案2:复用驱动默认序列化逻辑(更推荐)

手动遍历字段/属性容易遗漏驱动默认处理的细节(比如判别器、特殊类型序列化),如果你的序列化逻辑没有特殊需求,建议复用默认序列化器来处理,只在反序列化时做旧版兼容:

public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TypedFooBase<T> value) 
{
    // 使用驱动默认的序列化器处理,自动包含判别器等必要逻辑
    var defaultSerializer = BsonSerializer.LookupSerializer(typeof(TypedFooBase<T>));
    defaultSerializer.Serialize(context, args, value);
}

这种方式更可靠,能避免因手动处理导致的其他潜在问题。


额外建议:处理旧版无判别器的文档

如果你的旧版事件没有_t字段,还需要针对FooBase注册一个序列化器,在反序列化时先判断文档特征(比如Version字段),再决定实例化哪个子类:

public class FooBaseSerializer : SerializerBase<FooBase>
{
    public override FooBase Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        // 先读取整个文档为BsonDocument
        var doc = BsonDocumentSerializer.Instance.Deserialize(context, args);
        
        // 根据版本或字段判断是旧版还是新版
        if (doc.Contains("Version") && doc["Version"].AsInt32 < 2)
        {
            // 旧版:转换为TypedFooBase<TestEvent>,用你的自定义反序列化逻辑
            var expando = BsonTypeMapper.MapToDotNetValue(doc) as ExpandoObject;
            string valueString = JsonConvert.SerializeObject(expando);
            return JsonConvert.DeserializeObject<TypedFooBase<TestEvent>>(valueString, new Json.JsonAncientEventSerializer<TestEvent>()) 
                ?? throw new Exception("Failed to deserialize ancient event");
        }
        else
        {
            // 新版:根据_t判别器找到对应子类序列化器
            var discriminator = doc["_t"].AsString;
            var targetType = Type.GetType(discriminator);
            var serializer = BsonSerializer.LookupSerializer(targetType);
            return (FooBase)serializer.Deserialize(context, args);
        }
    }

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, FooBase value)
    {
        // 复用对应子类的序列化器
        var serializer = BsonSerializer.LookupSerializer(value.GetType());
        serializer.Serialize(context, args, value);
    }
}

// 注册这个序列化器
BsonSerializer.RegisterSerializer(typeof(FooBase), new FooBaseSerializer());

这样就能同时兼容旧版无判别器和新版有判别器的文档了。


内容的提问来源于stack exchange,提问作者TiGreX

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 08:37:45