Kostassoid
Kostassoid

Reputation: 1898

Storing Enums as strings in 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.

Upvotes: 91

Views: 63495

Answers (9)

Alexandre Daubricourt
Alexandre Daubricourt

Reputation: 4943

.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

Upvotes: 0

Chipo Hamayobe
Chipo Hamayobe

Reputation: 1057

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

Upvotes: 5

John Gietzen
John Gietzen

Reputation: 49554

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

Upvotes: 162

Bouke
Bouke

Reputation: 12158

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

Upvotes: 2

sboisse
sboisse

Reputation: 5528

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.

Upvotes: 9

wilver
wilver

Reputation: 2116

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

Upvotes: 7

Ricardo Rodriguez
Ricardo Rodriguez

Reputation: 1090

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

Upvotes: 71

boypula
boypula

Reputation: 81

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

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

Upvotes: 5

Wade Kaple
Wade Kaple

Reputation: 321

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.

Upvotes: 16

Related Questions