Serialize Hashset of enum in MongoDB - c#

I want to serialize in MongoDB, member Engines (HashSet of enum) as string to see the name as string and not a number
The class is :
public class EnginesPerFile
{
public Guid FileId { get; set; }
public string Path { get; set; }
public HashSet<EngineType> Engines { get; set; }
}
I am trying to map like this :
BsonClassMap.RegisterClassMap<EnginesPerFile>(cm =>
{
cm.AutoMap();
cm.MapIdField(c => c.FileId);
cm.MapMember(c => c.Engines).SetSerializer(new EnumSerializer<EngineType>(BsonType.String));
});
But i get an error:
Value type of serializer is EngineType and does not match member type System.Collections.Generic.HashSet`1[[Playground.Model.EngineType, Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].
Parameter name: serializer.
How can i serialize hashset of enum in MongoDB?

I fix it by writing custom Serializer: (EngineType can be a generic T)
public class MyHashSetSerializer : SerializerBase<HashSet<EngineType>>
{
private readonly IBsonSerializer _serializer =
BsonSerializer.LookupSerializer(typeof(EngineType));
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, HashSet<EngineType> value)
{
var set = (HashSet<EngineType>)value;
context.Writer.WriteStartDocument();
int i = 0;
foreach (var element in set)
{
context.Writer.WriteName($"[{i}]");
context.Writer.WriteString(element.ToString());
}
context.Writer.WriteEndDocument();
}
public override HashSet<EngineType> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var set = new HashSet<EngineType>();
context.Reader.ReadStartDocument();
while (context.Reader.ReadBsonType() != 0)
{
context.Reader.SkipName();
var element =
(EngineType)_serializer.Deserialize(context, args);
set.Add(element);
}
context.Reader.ReadEndDocument();
return set;
}
}
And then i register class map like this:
BsonClassMap.RegisterClassMap<EnginesPerFile>(cm =>
{
cm.AutoMap();
cm.MapIdField(c => c.FileId);
cm.MapMember(c => c.Engines).SetSerializer(new MyHashSetSerializer());
});

Related

Why is my custom member resolver for enum to class conversion not working?

I get the exception message:
The binary operator NotEqual is not defined for the types 'NotificationArea' and 'System.Object'
Source enum:
public enum NotificationArea
{
One,
Two,
Three
}
Destination class:
public class EnumValue
{
public EnumValue()
{
}
public EnumValue(Enum tEnum)
{
Id = Convert.ToInt32(tEnum);
Name = GetEnumValue(tEnum, tEnum.GetType());
}
public int Id { get; set; }
public string Name { get; set; }
public string GetEnumValue(Enum tEnum, Type type)
{
MemberInfo member = type.GetMember(tEnum.ToString())[0];
if (member.GetCustomAttribute<DisplayAttribute>() != null)
{
DisplayAttribute attribute = member.GetCustomAttribute<DisplayAttribute>();
return attribute.Name;
}
return tEnum.ToString();
}
}
Conversion class:
public class EnumConverter : IMemberValueResolver<Notification,
NotificationModel, NotificationArea, EnumValue>
{
public EnumValue Resolve(Notification source, NotificationModel destination,
NotificationArea sourceMember, EnumValue destMember, ResolutionContext context)
{
var model = source.Area.ToDisplay();
return model;
}
}
ToDisplay Extension:
public static EnumValue ToDisplay(this Enum value)
{
var display = new EnumValue(value);
return display;
}
Implementation:
// Query
var notifications = await _db.Notifications
.OrderByDescending(x => x.Timestamp)
.ProjectTo<NotificationModel>(_mapperConfig)
.ToListAsync(token);
// Mapping
CreateMap<Notification, NotificationModel>()
.ForMember(dest => dest.Area, opt => opt.ResolveUsing(src => src.Area));
Source and Destination Models:
public class Notification
{
public int Id {get;set;}
public NotificationArea Area { get; set; }
}
public class NotificationModel
{
public int Id {get;set;}
public EnumValue Area {get;set;}
}
I know I'm doing something wrong... obviously... I just don't know exactly where it's at. I feel Like I have A and C but am missing B.

Call Mapper.Map inside a custom type converter

I know this question has been asked once
here but there wasn't necessary to call Automapper.Map() method inside custom type convertor. So what if I have this type of structure:
class MyType
{
public double ValueToBeComputed1 { get; set; }
public double ValueToBeComputed2 { get; set; }
}
class ComplexType
{
public double ValueToBeComputed { get; set; }
public MyType MyProperty { get; set; }
}
For all the values to be computed I need to make different calculus so I will have a custom type convertor for Complex type to let's say OtherType. My question is if I will be able to call Mapper.Map() for the property MyProperty inside that custom converter?
After I faced the outdated documentation for Automapper custom type converter, where the ITypeConverter interface has been changed, and I found the answer here:
ITypeConverter interface has been changed in AutoMapper 2.0 , I was able to make a working prototype which produces converted types as follows:
public class ComplexTypeConverter : ITypeConverter<ComplexSourceType, ComplexDestinationType>
{
public ComplexDestinationType Convert(ResolutionContext context)
{
var source = (ComplexSourceType)context.SourceValue;
return new ComplexDestinationType
{
MyProperty = Mapper.Map<SourceType, DestinationType>(source.MyProperty),
ValueComputed = source.ValueToBeComputed + 10
};
}
}
public class TypeConverter : ITypeConverter<SourceType, DestinationType>
{
public DestinationType Convert(ResolutionContext context)
{
var source= (SourceType)context.SourceValue;
return new DestinationType
{
ValueComputed1 = source.ValueToBeComputed1 + 10,
ValueComputed2 = source.ValueToBeComputed2 + 10
};
}
}
class Program
{
static void Main(string[] args)
{
Mapper.Initialize(cfg => {
cfg.CreateMap<ComplexSourceType, ComplexDestinationType>().ConvertUsing(new ComplexTypeConverter());
cfg.CreateMap<SourceType, DestinationType>().ConvertUsing(new TypeConverter());
});
Mapper.AssertConfigurationIsValid();
ComplexSourceType source = new ComplexSourceType
{
MyProperty = new SourceType
{
ValueToBeComputed1 = 1,
ValueToBeComputed2 = 1
},
ValueToBeComputed = 1
};
var dest = Mapper.Map<ComplexSourceType, ComplexDestinationType>(source);
}
}
The dest object holds the modified data with 11 on each property

Designing a custom PetaPoco mapper that supports HeadSprings Enumeration Class

I'm attempting to create a mapper so PetaPoco can hydrate and persist POCOs with Enumeration class properties. See more about Enumeration classes here or here.
For instance, Take this class.
public class PetType : Headspring.Enumeration<PetType>
{
public static readonly PetType Frog = new PetType(1, "Frog");
public static readonly PetType Cat = new PetType(2, "Cat");
public static readonly PetType Fish = new PetType(3, "Fish");
public static readonly PetType Dog = new PetType(4, "Dog");
private PetType(int value, string displayName) : base(value, displayName) { }
}
Which can be used like so:
var MyPet = PetType.Dog;
Here is the Poco I want to hydrate/persist with the database:
public class Pet
{
public int ID { get; set; }
public string OwnerName { get; set; }
public DateTime DateOfBirth { get; set; }
public string PetName{ get; set; }
public PetType PetType{ get; set; }
}
I have designed a custom mapper that will work with PetType:
class EnumClassMapper : PetaPoco.StandardMapper
{
public override Func<object, object> GetFromDbConverter(System.Reflection.PropertyInfo targetProperty, Type sourceType)
{
if (targetProperty.PropertyType == typeof(PetType))
{
return (x) => PetType.FromValue((int) x);
}
return base.GetFromDbConverter(targetProperty, sourceType);
}
public override Func<object, object> GetToDbConverter(System.Reflection.PropertyInfo sourceProperty)
{
if (sourceProperty.PropertyType == typeof(PetType))
{
return (x) => ((PetType)x).Value;
}
return base.GetToDbConverter(sourceProperty);
}
}
However suppose I create another Enumeration subclass for disposition.
public class Disposition: Headspring.Enumeration<Disposition>
{
public static readonly Friendly = new Disposition(1, "Friendly");
public static readonly Timid = new Disposition(2, "Timid");
public static readonly Aggressive = new Disposition(3, "Aggressive");
private Disposition(int value, string displayName) : base(value, displayName) { }
}
I don't want to have to update my mapper every time I create a new subclass of the Enumeration class. I prefer that the mapping code could recognize that the property type is a descendent of the Enumeration class, and map accordingly. I assume the answer is to make use of reflection, but I don't know how to proceed.
What about
public class EnumClassMapper<T> : PetaPoco.StandardMapper
where T : Headspring.Enumeration<T>
{
public override Func<object, object> GetFromDbConverter(System.Reflection.PropertyInfo targetProperty, Type sourceType)
{
return (x) => Enumeration<T, int>.FromValue((int) x);
}
public override Func<object, object> GetToDbConverter(System.Reflection.PropertyInfo sourceProperty)
{
return (x) => ((T)x).Value;
}
}
var builder = DatabaseConfiguration.Build()
.UsingConnectionStringName("sqlite")
.UsingDefaultMapper<ConventionMapper>(m =>
{
m.FromDbConverter = (targetProperty, sourceType) =>
{
if (targetProperty == null)
return null;
var t = targetProperty.PropertyType;
if (t.BaseType == null || ! t.BaseType.IsGenericType)
return null;
if (t.BaseType.GetGenericTypeDefinition() != typeof(Headspring.Enumeration<>))
return null;
return ((IMapper)Activator.CreateInstance(typeof(EnumClassMapper<>).MakeGenericType(t))).GetFromDbConverter(targetProperty, sourceType);
};
m.ToDbConverter = sourceProperty =>
{
if (sourceProperty == null)
return null;
var t = sourceProperty.PropertyType;
if (t.BaseType == null || !t.BaseType.IsGenericType)
return null;
if (t.BaseType.GetGenericTypeDefinition() != typeof(Headspring.Enumeration<>))
return null;
return ((IMapper)Activator.CreateInstance(typeof(EnumClassMapper<>).MakeGenericType(t))).GetToDbConverter(sourceProperty);
};
});
var db = builder.Create();

"Unable to determine the serialization information for" error on MongoDB complex type equal to null filter

I am getting the below error while trying to run equal filter against null for a complex type object in MongoDB.Driver 2.0:
InvalidOperationException: Unable to determine the serialization information for
e => e.Deletion.
at MongoDB.Driver.ExpressionFieldDefinition2.Render(IBsonSerializer1 docume
ntSerializer, IBsonSerializerRegistry serializerRegistry)
at MongoDB.Driver.SimpleFilterDefinition2.Render(IBsonSerializer1 documentS
erializer, IBsonSerializerRegistry serializerRegistry)
at MongoDB.Driver.AndFilterDefinition1.Render(IBsonSerializer1 documentSeri
alizer, IBsonSerializerRegistry serializerRegistry)
at MongoDB.Driver.MongoCollectionImpl1.FindOneAndUpdateAsync[TProjection](Fi
lterDefinition1 filter, UpdateDefinition1 update, FindOneAndUpdateOptions2 op
tions, CancellationToken cancellationToken)
This is the filter:
Builders<TEntity>.Filter.Eq(e => e.Deletion, null)
In order to reproduce, run the following code with MongoDB.Driver 2.0.0 version:
public sealed class OccuranceWithReason
{
public OccuranceWithReason() : this(null)
{
}
public OccuranceWithReason(string reason)
{
Reason = reason;
OccuredOn = DateTime.UtcNow;
}
public string Reason { get; private set; }
public DateTime OccuredOn { get; private set; }
}
public interface IDeletable
{
OccuranceWithReason Deletion { get; }
}
public abstract class BaseEntity : IDeletable
{
protected BaseEntity()
{
Id = ObjectId.GenerateNewId().ToString();
}
public string Id { get; private set; }
public int PeekedCount { get; set; }
public OccuranceWithReason Deletion { get; private set; }
}
public class FooEntity : BaseEntity
{
}
class Program
{
static void Main(string[] args)
{
MongoConfig.Configure();
var client = new MongoClient();
var db = client.GetDatabase("foo");
var fooCol = db.GetCollection<FooEntity>("foos");
var foo = PeekForInsertSync(fooCol);
}
public static TEntity PeekForInsertSync<TEntity>(IMongoCollection<TEntity> collection)
where TEntity : BaseEntity
{
var query = Builders<TEntity>.Filter.And(
Builders<TEntity>.Filter.Eq(e => e.Deletion, null),
Builders<TEntity>.Filter.Lte(e => e.PeekedCount, 10)
);
return collection.Find(query).FirstOrDefaultAsync().Result;
}
}
internal static class MongoConfig
{
public static void Configure()
{
RegisterConventions();
RegisterGlobalSerializationRules();
ConfigureEntities();
ConfigureValueObjects();
}
private static void RegisterConventions()
{
var pack = new ConventionPack { new CamelCaseElementNameConvention(), new IgnoreIfNullConvention(false) };
ConventionRegistry.Register("all", pack, t => true);
}
private static void RegisterGlobalSerializationRules()
{
BsonSerializer.UseNullIdChecker = true;
}
private static void ConfigureEntities()
{
BsonClassMap.RegisterClassMap<BaseEntity>(cm =>
{
cm.MapMember(c => c.Id).SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.SetIdMember(cm.GetMemberMap(c => c.Id));
});
}
private static void ConfigureValueObjects()
{
BsonClassMap.RegisterClassMap<OccuranceWithReason>(cm =>
{
cm.AutoMap();
cm.MapCreator(occurance => new OccuranceWithReason(occurance.Reason));
});
}
}
Any idea?
The problem is about the BaseEntity object serialization registration. Deletion property was not mapped. Auto mapping all fields solved the problem:
BsonClassMap.RegisterClassMap<BaseEntity>(cm =>
{
cm.AutoMap();
cm.MapMember(c => c.Id).SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.SetIdMember(cm.GetMemberMap(c => c.Id));
});

Serialize entity class containing IMongoQuery

My entity class is like this:
public class MyType
{
public string Name { get; set; }
public IMongoQuery MyQuery { get; set; }
}
I am not able to persist this with the default serializers, when MyQuery contains anything complex such as an $in.
BsonDocumentSerializer gives error:
Element name '$in' is not valid because it starts with a '$'.
I assume I need a special serializer type attributed to MyQuery. I've tried BsonDocument, BsonString, BsonJavaScript - all cannont cast to MongoDB.Driver.QueryDocument, which is the type of object stored in MyQuery.
Does this require a custom IBsonSerializer?
Works for C# driver 2.0:
public class MyIMongoSerializer : SerializerBase<IMongoQuery>
{
public override void Serialize(BsonSerializationContext context,
BsonSerializationArgs args,
IMongoQuery value)
{
if (value == null)
{
context.Writer.WriteNull();
}
else
{
var query = (IMongoQuery)value;
var json = query.ToJson();
context.Writer.WriteString(json);
}
}
public override IMongoQuery Deserialize(BsonDeserializationContext context,
BsonDeserializationArgs args)
{
if (context.Reader.GetCurrentBsonType() == BsonType.Null)
{
context.Reader.ReadNull();
return null;
}
else
{
var value = context.Reader.ReadString();
var doc = BsonDocument.Parse(value);
var query = new QueryDocument(doc);
return query;
}
}
}
And annotate the property to use the serializer:
[BsonSerializer(typeof(MyIMongoSerializer))]
public IMongoQuery filter { get; set; }
This seems to work. Stores the query as a JSON string.
public class QueryDocumentSerializer : BsonBaseSerializer
{
public override object Deserialize(MongoDB.Bson.IO.BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
{
if (bsonReader.GetCurrentBsonType() == BsonType.Null)
{
bsonReader.ReadNull();
return null;
}
else
{
var value = bsonReader.ReadString();
var doc = BsonDocument.Parse(value);
var query = new QueryDocument(doc);
return query;
}
}
public override void Serialize(MongoDB.Bson.IO.BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
{
if (value == null)
{
bsonWriter.WriteNull();
}
else
{
var query = (QueryDocument)value;
var json = query.ToJson();
bsonWriter.WriteString(json);
}
}
}

Categories