MongoDb C# driver - serializing List<enum> as string[]
Asked Answered
V

3

5

I have a class with a property of type List<SomeEnum>. Something like this:

public enum MyEnum
{
  A,
  B
}

public class MyClass
{
    public string Id { get; set; }
    public List<MyEnum> Values { get; set; }
}

I'm already using the EnumRepresentationConvention in this way:

ConventionRegistry.Register("EnumStringConvention", new ConventionPack { new EnumRepresentationConvention(BsonType.String) }, t => true);

Still, the Values property gets serialized as an array of ints (simple enum properties are correctly handled as ints). It seem the convention is not used in the context of the list serialization.

How can I enforce the serializer to write strings instead of ints?

Valentia answered 15/11, 2017 at 16:50 Comment(3)
I just created an issue for that topic at jira.mongodb.org/browse/CSHARP-2096 and also a corresponding PR that ships the required fix/improvement: github.com/mongodb/mongo-csharp-driver/pull/305Reamer
You're the man! Thanks.Valentia
You're very welcome - let's see if it gets merged.Reamer
A
7

Instead of calling ConventionRegistry.Register(), add data annotation [BsonRepresentation(BsonType.String)] to the property Values of MyClass.

public class MyClass
{
    public string Id { get; set; }

    [BsonRepresentation(BsonType.String)] 
    public List<MyEnum> Values { get; set; }
}

After that change collection.InsertOne(obj); is saving this:

{
    "_id" : "1",
    "Values" : [ 
        "A", 
        "B"
    ]
}
Atronna answered 15/11, 2017 at 17:52 Comment(2)
It works flawlessly. Thanks for the workaround, waiting for the PR by @dnikless to land into production.Valentia
@Daniel How to use this via class map?Baudekin
S
4

Alas this PR has been closed as won't fix. https://github.com/mongodb/mongo-csharp-driver/pull/305#issuecomment-731475503.

Good reasons are given for this, but it is the kind of thing that I think people want to do, so depending on your appetite for jumping through hoops you can try this (.NET 5):

using System;
using System.Collections.Generic;
using System.Reflection;

using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;

public sealed class EnumWrapper<TEnum>
    where TEnum : struct, Enum
{
    [BsonConstructor]
    public EnumWrapper(TEnum value) => this.Value = value;

    public TEnum Value { get; }

    public static readonly IBsonSerializer<EnumWrapper<TEnum>> Serializer = new BsonSerializerImpl();

    public static implicit operator TEnum(EnumWrapper<TEnum> wrapper) => wrapper.Value;

    public static implicit operator EnumWrapper<TEnum>(TEnum value) => new(value);

    public override bool Equals(object obj) =>
        obj is EnumWrapper<TEnum> wrapper
        && EqualityComparer<TEnum>.Default.Equals(this.Value, wrapper.Value);

    public override int GetHashCode() => HashCode.Combine(this.Value);

    public override string ToString() => this.Value.ToString();

    private class BsonSerializerImpl : IBsonSerializer<EnumWrapper<TEnum>>
    {
        public Type ValueType => typeof(EnumWrapper<TEnum>);

        public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, EnumWrapper<TEnum> value) =>
            context.Writer.WriteString(((TEnum)value).ToString());

        public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) =>
            this.Serialize(context, args, (EnumWrapper<TEnum>)value);

        object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
            this.Deserialize(context, args);

        public EnumWrapper<TEnum> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
            Enum.Parse<TEnum>(context.Reader.ReadString());
    }
}

public class EnumWrapperBsonSerializationProvider : IBsonSerializationProvider
{
    public IBsonSerializer GetSerializer(Type type)
    {
        if (!type.IsGenericType)
        {
            return null;
        }

        var typeDefinition = type.GetGenericTypeDefinition();
        if (typeDefinition != typeof(EnumWrapper<>))
        {
            return null;
        }

        var field = type.GetField(nameof(EnumWrapper<Hack>.Serializer), BindingFlags.Public | BindingFlags.Static);
        return (IBsonSerializer)field.GetValue(null);
    }

    private enum Hack { }
}

Now you can use EnumWrapper<TEnum> mostly wherever you would normally use TEnum and it will do what you want. If you don't register the serialization provider it will serialize as nested objects, so before doing anything you should call this:

BsonSerializer.RegisterSerializationProvider(new EnumWrapperBsonSerializationProvider());
Skipp answered 3/1, 2021 at 13:3 Comment(0)
P
1

This code solves the serialize collection-of-enum as array-of-strings problem, looks simple but took awhile to figure out:

call

BsonSerializer.RegisterSerializationProvider(new EnumProvider());

before any other code BSON code, where EnumProvider is:

class EnumProvider : IBsonSerializationProvider
{
    public static IBsonSerializer CreateClass<T>() where T : struct, Enum => new EnumSerializer<T>(BsonType.String);

    private static readonly MethodInfo s_mi = typeof(EnumProvider).GetMethod(nameof(CreateClass));
    public IBsonSerializer GetSerializer(Type type)
        => type.IsEnum ? (IBsonSerializer)s_mi.MakeGenericMethod(type).Invoke(null, null) : null;
}

don't worry about performance of using reflection here, this code is called once per enum type

Proctology answered 9/7, 2023 at 11:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.