在 MongoDB 中将枚举存储为字符串

发布于 2024-11-29 01:36:35 字数 491 浏览 7 评论 0原文

有没有办法将枚举存储为字符串名称而不是序数值?

示例:

假设我有这个枚举:

public enum Gender
{
    Female,
    Male
}

现在,如果某个虚构的用户存在,

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

它将以 { ... "Gender" : 1 ... } 的形式存储在 MongoDb 数据库中,

但我更喜欢这样的 { .. . “性别” : “男” ... }

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

我的背景:我在 POCO 上使用强类型集合(嗯,我标记 AR 并偶尔使用多态性)。我有一个工作单元形式的薄数据访问抽象层。所以我不会序列化/反序列化每个对象,但我可以(并且确实)定义一些 ClassMap。我使用官方 MongoDb 驱动程序 + Fluent-mongodb。

Is there a way to store Enums as string names rather than ordinal values?

Example:

Imagine I've got this enum:

public enum Gender
{
    Female,
    Male
}

Now if some imaginary User exists with

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

it'll be stored in MongoDb database as { ... "Gender" : 1 ... }

but i'd prefer something like this { ... "Gender" : "Male" ... }

Is this possible? Custom mapping, reflection tricks, whatever.

My context: I use strongly typed collections over POCO (well, I mark ARs and use polymorphism occasionally). I've got a thin data access abstraction layer in a form of Unit Of Work. So I'm not serializing/deserializing each object but I can (and do) define some ClassMaps. I use official MongoDb driver + fluent-mongodb.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

好菇凉咱不稀罕他 2024-12-06 01:36:35
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; }
}
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; }
}
北笙凉宸 2024-12-06 01:36:35

MongoDB .NET 驱动程序允许您应用约定来确定确定的程度处理 CLR 类型和数据库元素之间的映射。

如果您希望将此应用于所有枚举,则只需为每个 AppDomain 设置一次约定(通常在启动应用程序时),而不是向所有类型添加属性或手动映射每种类型:

// Set up MongoDB conventions
var pack = new ConventionPack
{
    new EnumRepresentationConvention(BsonType.String)
};

ConventionRegistry.Register("EnumStringConvention", pack, t => true);

The MongoDB .NET Driver lets you apply conventions to determine how certain mappings between CLR types and database elements are handled.

If you want this to apply to all your enums, you only have to set up conventions once per AppDomain (usually when starting your application), as opposed to adding attributes to all your types or manually map every type:

// Set up MongoDB conventions
var pack = new ConventionPack
{
    new EnumRepresentationConvention(BsonType.String)
};

ConventionRegistry.Register("EnumStringConvention", pack, t => true);
伪心 2024-12-06 01:36:35

您可以为包含枚举的类自定义类映射,并指定该成员由字符串表示。这将处理枚举的序列化和反序列化。

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

         });
      }

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

You can customize the class map for the class that contains the enum and specify that the member be represented by a string. This will handle both the serialization and deserialization of the enum.

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

         });
      }

I am still looking for a way to specify that enums be globally represented as strings, but this is the method that I am currently using.

a√萤火虫的光℡ 2024-12-06 01:36:35

我发现仅应用 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,它将解析装箱值的 TypeCodeInt32用于枚举值),并使用该类型将枚举值存储在数据库中。因此装箱的枚举值最终被序列化为 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)存储枚举类型及其字符串值。

I have found that just applying Ricardo Rodriguez' answer is not sufficient in some cases to properly serialize enum values to string into MongoDb:

// Set up MongoDB conventions
var pack = new ConventionPack
{
    new EnumRepresentationConvention(BsonType.String)
};

ConventionRegistry.Register("EnumStringConvention", pack, t => true);

If your data structure involves enum values being boxed into objects, the MongoDb serialization will not use the set EnumRepresentationConvention to serialize it.

Indeed, if you look at the implementation of MongoDb driver's ObjectSerializer, it will resolve the TypeCode of the boxed value (Int32 for enum values), and use that type to store your enum value in the database. So boxed enum values end up being serialized as int values. They will remain as int values when being deserialized as well.

To change this, it's possible to write a custom ObjectSerializer that will enforce the set EnumRepresentationConvention if the boxed value is an enum. Something like this:

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

and then set the custom serializer as the one to use for serializing objects:

BsonSerializer.RegisterSerializer(typeof(object), new ObjectSerializer());

Doing this will ensure boxed enum values will be stored as strings just like the unboxed ones.

Keep in mind however that when deserializing your document, the boxed value will remain a string. It will not be converted back to the original enum value. If you need to convert the string back to the original enum value, a discrimination field will likely have to be added in your document so the serializer can know what is the enum type to desrialize into.

One way to do it would be to store a bson document instead of just a string, into which the discrimination field (_t) and a value field (_v) would be used to store the enum type and its string value.

萌吟 2024-12-06 01:36:35

使用驱动程序 2.x,我使用 特定序列化器

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

With driver 2.x I solved using a specific serializer:

BsonClassMap.RegisterClassMap<Person>(cm =>
            {
                cm.AutoMap();
                cm.MapMember(c => c.Gender).SetSerializer(new EnumSerializer<Gender>(BsonType.String));
            });
甜嗑 2024-12-06 01:36:35

使用 MemberSerializationOptionsConvention 定义有关如何保存枚举的约定。

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

Use MemberSerializationOptionsConvention to define a convention on how an enum will be saved.

new MemberSerializationOptionsConvention(typeof(Gender), new RepresentationSerializationOptions(BsonType.String))
谈情不如逗狗 2024-12-06 01:36:35

如果您使用 .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; }
}

If you are using .NET Core 3.1 and above, use the latest ultra-fast Json Serializer/Deserializer from Microsoft, System.Text.Json (https://www.nuget.org/packages/System.Text.Json).

See the metrics comparison at 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; }
}
々眼睛长脚气 2024-12-06 01:36:35

此处发布的答案适用于 TEnumTEnum[],但不适用于 Dictionary。您可以在使用代码初始化序列化器时实现此目的,但是我想通过属性来实现此目的。我创建了一个灵活的 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; }

The answers posted here work well for TEnum and TEnum[], however won't work with Dictionary<TEnum, object>. You could achieve this when initializing serializer using code, however I wanted to do this through attributes. I've created a flexible DictionarySerializer that can be configured with a serializer for the key and value.

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) { }
}

Usage like this, where both key and value are enum types, but could be any combination of serializers:

    [BsonSerializer(typeof(DictionarySerializer<
        Dictionary<FeatureToggleTypeEnum, LicenseFeatureStateEnum>, 
        EnumStringSerializer<FeatureToggleTypeEnum>,
        EnumStringSerializer<LicenseFeatureStateEnum>>))]
    public Dictionary<FeatureToggleTypeEnum, LicenseFeatureStateEnum> FeatureSettings { get; set; }
温柔戏命师 2024-12-06 01:36:35

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

注意:

  • 如果您想自动注册程序集中的所有枚举,您可以考虑创建自己的枚举属性以标记它们,而不是盲目地全部获取它们
  • 这是使用自定义枚举序列化的好模板案例,我使用蛇形案例,所以约定包无法工作

.NET 7.0

Improving on @sboisse very good answer, I've found a way that satisfies all of my usecases.

Generic Enum Serializer

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

Registering every enum in assembly

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

Notes:

  • If you want to auto register all enums in assembly you can consider creating your own enum attribute in order to mark them rather than blindly taking them all
  • This is a good template to use your custom enum serialization case, I use snake case so convention pack cannot work
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文