在MongoDB中以字符串形式存储枚举

本文关键字:存储 枚举 字符串 MongoDB | 更新日期: 2023-09-27 18:05:12

是否有一种方法将枚举存储为字符串名称而不是序数值?

的例子:

假设我有这个enum:

public enum Gender
{
    Female,
    Male
}

现在如果有一个假想的用户存在

...
Gender gender = Gender.Male;
...

它将存储在MongoDb数据库中为{…"性别":1……}

但是我更喜欢这样的{…"性别":"男性"……}

这可能吗?自定义映射,反射技巧,等等。

我的上下文:我在POCO上使用强类型集合(好吧,我标记ar并偶尔使用多态性)。我有一个工作单元形式的瘦数据访问抽象层。所以我没有序列化/反序列化每个对象,但我可以(也确实)定义一些classmap。我使用官方的MongoDb驱动+ fluent-mongodb

在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 Driver允许您应用约定来确定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);
         });
      }

我仍然在寻找一种方法来指定枚举全局表示为字符串,但这是我目前使用的方法。

我发现,只是应用里卡多罗德里格斯的答案是不够的,在某些情况下,正确序列化枚举值字符串到MongoDb:

// Set up MongoDB conventions
var pack = new ConventionPack
{
    new EnumRepresentationConvention(BsonType.String)
};
ConventionRegistry.Register("EnumStringConvention", pack, t => true);

如果你的数据结构包含枚举值被装箱成对象,MongoDb序列化将不会使用set EnumRepresentationConvention来序列化它。

事实上,如果你看一下MongoDb驱动程序的ObjectSerializer的实现,它将解析盒装值的TypeCode(枚举值的Int32),并使用该类型将枚举值存储在数据库中。因此盒装枚举值最终被序列化为int值。在反序列化时,它们也将保持为int值。

要改变这种情况,可以编写自定义ObjectSerializer,如果盒装值是enum,则强制设置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)将用于存储enum类型及其字符串值。

与驱动程序2。我使用一个特定的序列化器来解决:

BsonClassMap.RegisterClassMap<Person>(cm =>
            {
                cm.AutoMap();
                cm.MapMember(c => c.Gender).SetSerializer(new EnumSerializer<Gender>(BsonType.String));
            });

使用MemberSerializationOptionsConvention定义如何保存enum的约定。

new MemberSerializationOptionsConvention(typeof(Gender), new RepresentationSerializationOptions(BsonType.String))

如果你使用的是。net Core 3.1及以上版本,请使用微软最新的超快速Json序列化器/反序列化器,System.Text.Json (https://www.nuget.org/packages/System.Text.Json)。

查看指标比较:https://medium.com/@samichkhachkhi/system-text- jsonvs-newtonsoftjsond01935068143

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; }
}

这里发布的答案适用于TEnumTEnum[],但不适用于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; }

。NET 7.0

改进@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);
}

指出:

  • 如果你想在汇编中自动注册所有枚举,你可以考虑创建自己的enum属性,以便标记它们,而不是盲目地将它们全部取走
  • 这是一个使用自定义枚举序列化案例的好模板,我使用蛇形案例,因此convention pack无法工作