MongoDB自定义序列化器引发「Cannot create an abstract class」错误排查
问题分析与解决方案
咱们来一步步拆解你遇到的问题,找到根源并给出修复方案:
核心原因:缺失类型判别器导致驱动无法识别具体子类
当你注册自定义序列化器后,新版事件存入数据库时丢失了MongoDB驱动默认的类型判别器字段_t,这直接引发了两个关键问题:
- 读取时,驱动看到的文档没有
_t标识,无法判断该实例化哪个FooBase的具体子类,只能尝试直接实例化抽象类FooBase,于是抛出「Cannot create an abstract class」错误。 - 因为驱动没识别出这是
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
相关产品推荐
相关产品推荐

