In a desperate attempt of finding a solution quickly, I went through many stackoverflow/internet blogs for a whole day for the problem in the title of this Q/A.
There are already posted questions similar to this title but they aren't the same. Then it was obvious that I had to find the solution myself. Posting my findings and approach here, so that it could help someone (or me. I keep forgetting my own solutions, and chances are I might end up on this very same post again in distant future :) )
Issue: Getting exception similar to the one below
System.FormatException HResult=0x80131537 Message=An error occurred while deserializing the EventsToPublish field of class Domain.SeedWork.Aggregate
1[[System.String, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]: Type 'DomainManagedList
1[[Domain.Events.EventToPublish, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' does not have a suitable constructor or Add method. Source=MongoDB.Bson
StackTrace: at MongoDB.Bson.Serialization.BsonClassMapSerializer1.DeserializeMemberValue(BsonDeserializationContext context, BsonMemberMap memberMap) at MongoDB.Bson.Serialization.BsonClassMapSerializer
1.DeserializeClass(BsonDeserializationContext context) at MongoDB.Bson.Serialization.BsonClassMapSerializer1.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize[TValue](IBsonSerializer
1 serializer, BsonDeserializationContext context) at MongoDB.Driver.Core.Operations.CursorBatchDeserializationHelper.DeserializeBatch[TDocument](RawBsonArray batch, IBsonSerializer1 documentSerializer, MessageEncoderSettings messageEncoderSettings) at MongoDB.Driver.Core.Operations.FindOperation
1.CreateFirstCursorBatch(BsonDocument cursorDocument) at MongoDB.Driver.Core.Operations.FindOperation1.CreateCursor(IChannelSourceHandle channelSource, IChannelHandle channel, BsonDocument commandResult) at MongoDB.Driver.Core.Operations.FindOperation
1.d__129.MoveNext() at MongoDB.Driver.Core.Operations.FindOperation1.<ExecuteAsync>d__128.MoveNext() at MongoDB.Driver.OperationExecutor.<ExecuteReadOperationAsync>d__3
1.MoveNext() at MongoDB.Driver.MongoCollectionImpl1.<ExecuteReadOperationAsync>d__99
1.MoveNext() at MongoDB.Driver.MongoCollectionImpl1.<UsingImplicitSessionAsync>d__107
1.MoveNext() at Infrastructure.MongoDb.Repositories.MongoRepository2.<FindAsync>d__8.MoveNext() in C:\dev\domain-driven-customer-service\src\Infrastructure\MongoDb\Repositories\MongoRepository.cs:line 65 at Infrastructure.MongoDb.Repositories.Repository
2.d__3.MoveNext() in C:\dev\domain-driven-customer-service\src\Infrastructure\MongoDb\Repositories\Repository.cs:line 25 at Api.Program.d__0.MoveNext() in C:\dev\domain-driven-customer-service\src\Api\Program.cs:line 36This exception was originally thrown at this call stack: [External Code]
Inner Exception 1: BsonSerializationException: Type 'Domain.Aggregates.DomainManagedList`1[[Domain.Events.EventToPublish, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' does not have a suitable constructor or Add method.
CodePudding user response:
For me this issue occurred because I replaced generic List type in List<T> EventsToPublish { get; set;}
with my custom list type DomainManagedList
which had prohibited access methods that could add or modify anything from the list (Much like ImmutableList<T>
). The reason 'Why?' is not important for this post but the short reason is because we were implementing Domain Driven design aggregate pattern and needed Domain entities with limited access so that they could only be modified from aggregate itself (and not from application layer).
So the code for Type without fix looks like below.
public class DomainManagedList<T> : IEnumerable<T>
{ // Custom type that broke deserialization
private readonly List<T> _itemList;
public DomainManagedList()
{
_itemList = new List<T>();
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)_itemList).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
internal void Add(T item)
{
_itemList.Add(item);
}
internal void Remove(T item)
{
}
[InvokedOnDeserialization]
// ReSharper disable once UnusedMember.Global | Found via attribute and invoked using reflection
protected DomainManagedList(List<T> itemList)
{
_itemList = itemList;
}
}
Note that Add
method has internal
access modifier to prevent code from any other assembly to change list.
Given that mongoDB driver code is open source, I searched its github repository and found this file https://github.com/mongodb/mongo-csharp-driver/blob/master/src/MongoDB.Bson/Serialization/Serializers/EnumerableInterfaceImplementerSerializer.cs had this does not have a suitable constructor or Add method
line that was appearing in the exception.
So error was from EnumerableInterfaceImplementerSerializer
type. Upon going through this open source MongoDB driver repository, and little bit of internet searching on how to create custom deserializer I concluded that I need a custom BsonSerializer and override this deserialization finalizer that should allow finding non public method. Based on programming model we've followed it made more sense for us to have a protected
constructor and this example fix shows that approach; but you could easily tweak code to access any other methods to achieve same deserialization result.
Fix: We added protected constructor with custom attribute (not necessary for others) to deterministically find this constructor from custom Bson Serializer. Finally made sure that we register this serializer on app start. Full code as below.
Custom Attribute:
[AttributeUsage(AttributeTargets.Constructor)]
public class InvokedOnDeserializationAttribute : Attribute
{
}
Custom Type new definition: (Note: it includes protected constructor now.)
public class DomainManagedList<T> : IEnumerable<T>
{
private readonly List<T> _itemList;
public DomainManagedList()
{
_itemList = new List<T>();
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)_itemList).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
internal void Add(T item)
{
_itemList.Add(item);
}
internal void Remove(T item)
{
}
[InvokedOnDeserialization]
// ReSharper disable once UnusedMember.Global | Found via attribute and invoked using reflection
protected DomainManagedList(List<T> itemList)
{
_itemList = itemList;
}
}
Custom BsonSerializer:
public class DomainManagedListSerializer<TItem>
: EnumerableInterfaceImplementerSerializer<DomainManagedList<TItem>, TItem>
{
/// This class has custom deserializer based on suitable constructor.
/// To use this serializer you must register it using following method
/// BsonSerializer.RegisterGenericSerializerDefinition(typeof(DomainManagedList<>), typeof(DomainManagedListSerializer<>));
private readonly ConstructorInfo _constructorInfo;
public DomainManagedListSerializer()
{
_constructorInfo = typeof(DomainManagedList<TItem>)
.GetTypeInfo().GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(ci => Attribute.IsDefined((MemberInfo) ci, typeof(InvokedOnDeserializationAttribute)));
if (_constructorInfo != null)
{
var constructorParameters = _constructorInfo.GetParameters();
if (constructorParameters.Length == 1)
{
if (constructorParameters[0].ParameterType == typeof(List<TItem>))
{
return;
}
}
}
var message = string.Format("Type '{0}' does not have a suitable constructor that "
"implements '{1}'.",
typeof(DomainManagedList<TItem>).FullName,
nameof(InvokedOnDeserializationAttribute));
throw new BsonSerializationException(message);
}
protected override DomainManagedList<TItem> FinalizeResult(object accumulator)
{
return (DomainManagedList<TItem>)_constructorInfo.Invoke(new[] { accumulator });
}
}
Registering BsonSerializer (this goes after along with service registrations ):
BsonSerializer.RegisterGenericSerializerDefinition(typeof(DomainManagedList<>), typeof(DomainManagedListSerializer<>));