有没有办法将枚举存储为字符串名称而不是序数值?
示例:
想象我有这个枚举:
public enum Gender
{
Female,
Male
}
现在如果某个虚构的用户存在
...
Gender gender = Gender.Male;
...
它将作为 { ... "Gender" : 1 ... } 存储在 MongoDb 数据库中
但我更喜欢这样的东西 { ... "Gender" : "Male" ... }
这可能吗?自定义映射、反射技巧等等。
我的背景:我在 POCO 上使用强类型集合(嗯,我标记 AR 并偶尔使用多态性)。我有一个工作单元形式的薄数据访问抽象层。所以我不会序列化/反序列化每个对象,但我可以(并且确实)定义一些 ClassMap。我使用官方 MongoDb 驱动 + Fluent-mongodb。
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
public class Person
{
[JsonConverter(typeof(StringEnumConverter))] // JSON.Net
[BsonRepresentation(BsonType.String)] // Mongo
public Gender Gender { get; set; }
}
MongoDB .NET 驱动程序允许您应用约定来确定如何处理 CLR 类型和数据库元素之间的某些映射。
如果您希望这适用于所有枚举,则只需为每个 AppDomain 设置一次约定(通常在启动应用程序时),而不是向所有类型添加属性或手动映射每种类型:
// Set up MongoDB conventions
var pack = new ConventionPack
{
new EnumRepresentationConvention(BsonType.String)
};
ConventionRegistry.Register("EnumStringConvention", pack, t => true);
您可以为包含枚举的类自定义类映射,并指定该成员由字符串表示。这将处理枚举的序列化和反序列化。
if (!MongoDB.Bson.Serialization.BsonClassMap.IsClassMapRegistered(typeof(Person)))
{
MongoDB.Bson.Serialization.BsonClassMap.RegisterClassMap<Person>(cm =>
{
cm.AutoMap();
cm.GetMemberMap(c => c.Gender).SetRepresentation(BsonType.String);
});
}
我仍在寻找一种方法来指定枚举全局表示为字符串,但这是我当前正在使用的方法。
我发现仅应用 Ricardo Rodriguez' 答案在某些情况下不足以正确地将枚举值序列化为 MongoDb:
// Set up MongoDB conventions
var pack = new ConventionPack
{
new EnumRepresentationConvention(BsonType.String)
};
ConventionRegistry.Register("EnumStringConvention", pack, t => true);
如果您的数据结构涉及将枚举值装箱到对象中,则 MongoDb 序列化将不会使用集合
EnumRepresentationConvention
对其进行序列化。
事实上,如果您查看 MongoDb 驱动程序的 ObjectSerializer 的实现,它将解析装箱值的
TypeCode
(枚举值的 Int32
),并使用该类型将枚举值存储在数据库中。因此,装箱的枚举值最终被序列化为 int
值。反序列化时它们也将保留为 int
值。
要改变这一点,可以编写一个自定义
ObjectSerializer
,如果装箱值是枚举,它将强制执行集合 EnumRepresentationConvention
。像这样的东西:
public class ObjectSerializer : MongoDB.Bson.Serialization.Serializers.ObjectSerializer
{
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
{
var bsonWriter = context.Writer;
if (value != null && value.GetType().IsEnum)
{
var conventions = ConventionRegistry.Lookup(value.GetType());
var enumRepresentationConvention = (EnumRepresentationConvention) conventions.Conventions.FirstOrDefault(convention => convention is EnumRepresentationConvention);
if (enumRepresentationConvention != null)
{
switch (enumRepresentationConvention.Representation)
{
case BsonType.String:
value = value.ToString();
bsonWriter.WriteString(value.ToString());
return;
}
}
}
base.Serialize(context, args, value);
}
}
然后将自定义序列化器设置为用于序列化对象的序列化器:
BsonSerializer.RegisterSerializer(typeof(object), new ObjectSerializer());
这样做将确保装箱的枚举值将像未装箱的枚举值一样存储为字符串。
但请记住,反序列化文档时,装箱的值将保留为字符串。它不会被转换回原始枚举值。如果您需要将字符串转换回原始枚举值,则可能需要在文档中添加区分字段,以便序列化程序可以知道要反序列化的枚举类型。
一种方法是存储一个 bson 文档而不仅仅是一个字符串,其中区分字段 (
_t
) 和值字段 (_v
) 将用于存储枚举类型及其字符串值.
使用驱动程序 2.x,我使用特定序列化器解决了问题:
BsonClassMap.RegisterClassMap<Person>(cm =>
{
cm.AutoMap();
cm.MapMember(c => c.Gender).SetSerializer(new EnumSerializer<Gender>(BsonType.String));
});
使用 MemberSerializationOptionsConvention 定义有关如何保存枚举的约定。
new MemberSerializationOptionsConvention(typeof(Gender), new RepresentationSerializationOptions(BsonType.String))
如果您使用 .NET Core 3.1 及更高版本,请使用 Microsoft 最新的超快速 Json 序列化器/反序列化器 System.Text.Json (https://www.nuget.org/packages/System.Text.Json ).
请参阅指标比较:https://medium.com/@samichkhachkhi/system-text-json-vs-newtonsoft-json-d01935068143
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Text.Json.Serialization;;
public class Person
{
[JsonConverter(typeof(JsonStringEnumConverter))] // System.Text.Json.Serialization
[BsonRepresentation(BsonType.String)] // MongoDB.Bson.Serialization.Attributes
public Gender Gender { get; set; }
}
此处发布的答案适用于
TEnum
和 TEnum[]
,但不适用于 Dictionary<TEnum, object>
。您可以在使用代码初始化序列化器时实现此目的,但是我想通过属性来实现此目的。我创建了一个灵活的 DictionarySerializer
,可以使用键和值的序列化器进行配置。
public class DictionarySerializer<TDictionary, KeySerializer, ValueSerializer> : DictionarySerializerBase<TDictionary>
where TDictionary : class, IDictionary, new()
where KeySerializer : IBsonSerializer, new()
where ValueSerializer : IBsonSerializer, new()
{
public DictionarySerializer() : base(DictionaryRepresentation.Document, new KeySerializer(), new ValueSerializer())
{
}
protected override TDictionary CreateInstance()
{
return new TDictionary();
}
}
public class EnumStringSerializer<TEnum> : EnumSerializer<TEnum>
where TEnum : struct
{
public EnumStringSerializer() : base(BsonType.String) { }
}
像这样使用,其中键和值都是枚举类型,但可以是序列化器的任意组合:
[BsonSerializer(typeof(DictionarySerializer<
Dictionary<FeatureToggleTypeEnum, LicenseFeatureStateEnum>,
EnumStringSerializer<FeatureToggleTypeEnum>,
EnumStringSerializer<LicenseFeatureStateEnum>>))]
public Dictionary<FeatureToggleTypeEnum, LicenseFeatureStateEnum> FeatureSettings { get; set; }
改进@sboisse非常好的答案,我找到了一种满足我所有用例的方法。
// for boxed enums
BsonSerializer.RegisterSerializer(typeof(object), new BoxedEnumStringSerializer());
// for specifix unboxed enum
BsonSerializer.RegisterSerializer(typeof(MyEnum), new EnumStringSerializer<MyEnum>());
// serializer class
public class BoxedEnumStringSerializer : ObjectSerializer
{
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
{
var bsonWriter = context.Writer;
string? serialized = null;
if (value.GetType().IsEnum && value.ToString() is string valStr)
{
var conventions = ConventionRegistry.Lookup(value.GetType());
var enumRpz = conventions.Conventions.FirstOrDefault(convention => convention is EnumRepresentationConvention) as EnumRepresentationConvention;
switch (enumRpz?.Representation)
{
case BsonType.String:
serialized = valStr;
break;
}
}
if (serialized != null)
base.Serialize(context, args, serialized);
else
base.Serialize(context, args, value);
}
public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var val = context.Reader.ReadString();
return Enum.Parse(args.NominalType, val);
}
}
public class EnumStringSerializer<T> : BoxedEnumStringSerializer, IBsonSerializer<T> where T : struct, Enum
{
public new Type ValueType => typeof(T);
public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value)
{
base.Serialize(context, args, value);
}
T IBsonSerializer<T>.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
return (T)base.Deserialize(context, args);
}
}
var enums = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsEnum && t.IsPublic);
foreach (var e in enums)
{
var serializer = typeof(EnumStringSerializer<>).MakeGenericType(e);
BsonSerializer.RegisterSerializer(e, Activator.CreateInstance(serializer) as IBsonSerializer);
}
备注:
ConventionRegistry.Register(
name: "EnumRepresentation",
conventions: new ConventionPack { new EnumRepresentationConvention(BsonType.String) },
filter: _ => true);