1

I'm trying to make a selection on the Type field, CloudFileTypes is an enum. If you try to make a selection using linq methods or mongodb driver filter, then it perceives Type as a number and cannot find the desired file, and in the database in the Type field I have string values. And if I select some specific file and access the type field, then it will return the enumeration member, as needed. I apologize in advance if I misunderstood.

Problem part of code:

public CloudFile GetRoot(User user)
{
    var builder = Builders<CloudFile>.Filter;
    var query = builder.Eq(e => e.Type, CloudFileTypes.Root);
    var root = _fileCollection.Find(query).First();
    
    return root.First();
}

CloudFile model:

public class CloudFile
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; set; }
        [BsonElement("name")] public string Name { get; set; }
        [JsonConverter(typeof(JsonStringEnumConverter))]
        [BsonElement("type")] public CloudFileTypes Type { get; set; }
        [BsonElement("size")] public int Size { get; set; }
        [BsonElement("path")] public string Path { get; set; }
        [BsonRepresentation(BsonType.ObjectId)]
        [BsonElement("user")] public string User { get; set; }
        [BsonRepresentation(BsonType.ObjectId)]
        [BsonElement("parent")] public string Parent { get; set; }
        [BsonRepresentation(BsonType.ObjectId)]
        [BsonElement("childs")] public string[] Childs { get; set; }
    }
    public enum CloudFileTypes
    {
        [EnumMember(Value = "root")] Root,
        [EnumMember(Value = "folder")] Folder,
        [EnumMember(Value = "file")] File,
    }

Example of root file.

How do i solve this problem. Tried googling, didn't work.

EDIT: JsonConverter I created to serialize the enum when the object is sent to the client, using the extension from Yong Shun:

public class EnumStringConverter<TEnum> : JsonConverter where TEnum: struct, Enum
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(CloudFileTypes);
        }
        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
        {
            if (value is not TEnum @enum)
                return;

            if (!Attribute.IsDefined(@enum.GetType().GetMember(@enum.ToString()).FirstOrDefault(), typeof(EnumMemberAttribute)))
                writer.WriteValue(@enum.ToString());
            writer.WriteValue(EnumExtensions.GetEnumMemberValue(@enum));
        }
        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            var str = jo.ToString();
            var obj = EnumExtensions.EnumMemberValueToEnum<CloudFileTypes>(str);
            return obj;
        }
    }
Huhnak
  • 13
  • 3

1 Answers1

0

Tricky yet interesting question.

As mentioned that you are storing the enum value as string in the collection, based on your current query, MongoDB Fluent API will pass the enum value as an integer but not the string value from the EnumMemberAttribute.

Warning: This will be a long answer.

These are the steps in order to allow the MongoDB Fluent API to pass the value from the EnumMemberAttribute in the query.

  1. A helper/extension method that allows you to convert from string to Enum (which implements the EnumMemberAttribute and vice versa.
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

public static class EnumExtensions
{
    public static string GetEnumMemberValue<TEnum>(this TEnum @enum)
        where TEnum : struct, Enum
    {
        string enumMemberValue = @enum.GetType()
            .GetMember(@enum.ToString())
            .FirstOrDefault()?
            .GetCustomAttributes<EnumMemberAttribute>(false)
            .FirstOrDefault()?
            .Value;

        if (enumMemberValue == null)
            return @enum.ToString();
            //throw new ArgumentException($"Enum {@enum.GetType().Name} with member {@enum} not apply {nameof(EnumMemberAttribute)} attribute.");

        return enumMemberValue;
    }

    public static TEnum EnumMemberValueToEnum<TEnum>(this string value)
        where TEnum : struct, Enum
    {
        foreach (var field in typeof(TEnum).GetFields())
        {
            if (Attribute.GetCustomAttribute(field,
                typeof(EnumMemberAttribute)) is EnumMemberAttribute attribute)
            {
                if (attribute.Value == value)
                    return (TEnum)field.GetValue(null);
            }

            if (field.Name == value)
                return (TEnum)field.GetValue(null);
        }

        throw new ArgumentException($"{value} is not found.");
    }
}
  1. Implement and register a custom serializer for converting the string value (from EnumMemberAttribute value) to Enum and vice versa. Reference: @wilver's answer on Storing Enums as strings in MongoDB
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Serializers;

public class EnumMemberStringSerializer<TEnum> : ObjectSerializer, IBsonSerializer<TEnum>
    where TEnum : struct, Enum
{
    public new Type ValueType => typeof(TEnum);

    public TEnum Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return EnumExtensions.EnumMemberValueToEnum<TEnum>(base.Deserialize(context, args)?.ToString());
    }

    public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TEnum value)
    {
        base.Serialize(context, args, EnumExtensions.GetEnumMemberValue((TEnum)value));
    }

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
    {
        base.Serialize(context, args, EnumExtensions.GetEnumMemberValue((TEnum)value));
    }

    object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return EnumExtensions.EnumMemberValueToEnum<TEnum>(base.Deserialize(context, args)?.ToString());
    }
}
  1. Register the EnumMemberStringSerializer serializer (before IMongoDatabase & IMongoCollection instances are created).
BsonSerializer.RegisterSerializer(typeof(CloudFileTypes), new EnumMemberStringSerializer<CloudFileTypes>());
Yong Shun
  • 35,286
  • 4
  • 24
  • 46
  • Wow, thanks for such a quick and great answer, collection search works fine now, only serialization doesn't work for some reason when CloudFile object is sent back to the client, instead of the expected string it gets an int index of the enum. I removed attribute [JsonConverter(typeof(JsonStringEnumConverter))] from CloudFileType field in CloudFile and register serializer as you said. – Huhnak Jul 28 '23 at 15:24
  • 1
    I created a custom JsonConverter and added it to the field attributes. Everything is working now, thanks again! I attached the converter from above to the problem. – Huhnak Jul 28 '23 at 16:14
  • Can you explain why TEnum can be a struct, not only Enum? – Huhnak Jul 28 '23 at 18:31
  • Possible relate to this question: [Create Generic method constraining T to an Enum](https://stackoverflow.com/q/79126/8017690). If you use the methods from `Enum`, then it requires you to specify `where TEnum : struct`. For my case, as I didn't use `Enum` method, but just to extract the attribute from each member, thus the `struct` can be omitted. – Yong Shun Jul 29 '23 at 00:33