Custom validator sometimes generates a System.InvalidCastException when serializing the entity

Posts   
 
    
ChrisJCarlson avatar
Posts: 5
Joined: 14-Feb-2025
# Posted on: 14-Feb-2025 22:08:32   
Exception Message: System.InvalidCastException: Unable to cast object of type "Standard.EntityClasses.BaseValidator" to type "SingletonTypeWrapper"  
Stack  Trace:  
   at SD.LLBLGen.Pro.ORMSupportClasses.SerializationWriter.SingletonTypeWrapper.Equals(Object obj)  
   at System.Collections.Hashtable.get_Item(Object key)  
   at SD.LLBLGen.Pro.ORMSupportClasses.SerializationWriter.WriteTokenizedObject(Object value, Boolean recreateFromType)  
   at SD.LLBLGen.Pro.ORMSupportClasses.EntityBase2.SerializeOwnedData(SerializationWriter writer, Object context)  
   at SD.LLBLGen.Pro.ORMSupportClasses.FastSerializer.WriteUnreferencedEntity(EntityBase2 entity, BitVector32 serializationFlags)  
   at SD.LLBLGen.Pro.ORMSupportClasses.FastSerializer.WriteCollection(IFastSerializableEntityCollection2 collection, BitVector32 serializationFlags)  
   at SD.LLBLGen.Pro.ORMSupportClasses.FastSerializer.WriteEntityMemberCollections(EntityBase2 entity)  
   at SD.LLBLGen.Pro.ORMSupportClasses.FastSerializer.WriteReferencedEntities(Object root)  
   at SD.LLBLGen.Pro.ORMSupportClasses.FastSerializer.Serialize(EntityBase2 entity)  
   at SD.LLBLGen.Pro.ORMSupportClasses.FastSerializer.Serialize(Object root)  
   at SD.LLBLGen.Pro.ORMSupportClasses.SerializationHelper.Serialize(EntityBase2 entity, SerializationInfo info, StreamingContext context)  
   at SD.LLBLGen.Pro.ORMSupportClasses.EntityBase2.GetObjectData(SerializationInfo info, StreamingContext context)  
   at SD.LLBLGen.Pro.ORMSupportClasses.EntityCore`1.System.Runtime.Serialization.ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)  
   at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder)  
   at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder)  
   at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck)  
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck)  
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph)  
   at Standard.Extensions.EntityExtensions.CloneObject(Object toClone)  

We see this a lot when serializing entities over remoting. However it is not at all consistent. The same remoting call works most of the time. We also capture errors when it's just serializing the entity but not transmitting over a network. The error listed above is specifically from an attempt at cloning an entity. In which we use the System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serialize and copy the entity into a MemoryStream and then create a new object from that MemoryStream. In the above stack trace the CloneObject code is ours. This is the CloneObject code being called:

        private static object CloneObject(object toClone)
        {
            using (var ms = new MemoryStream())
            {
                var bf = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone));
                bf.Serialize(ms, toClone);
                ms.Seek(0, SeekOrigin.Begin);
                return bf.Deserialize(ms);
            }
        }

Reviewing the source code we received with our license I'm able to determine there is something going on with the way the IValidator gets serialized. The BaseValidator listed in the exception is our code that gets applied to every one of our entities via a partial class:

    public partial class CommonEntityBase
    {
        protected override IValidator CreateValidator()
        {
            return new BaseValidator();
        }

    }

It's largely there to handle sanitization of a view specific field types so they can save properly.

I suspect the exception is happening in equals overload of the SingletonTypeWrapper:

    public override bool Equals(object obj)
    {
        return _wrappedType.Equals(((SingletonTypeWrapper)obj)._wrappedType);
    }

Reading through the stack trace it seems the WriteTokenizedObject method is attempting to do a object lookup from the _objectTokenLookup Hashtable by using the validator class as the key. In this case it looks to me that it is using our BaseValidator as the key in the hash table causing each object in that hash table to do a comparison. But when we one of the SingletonTypeWrapper's in the hash table executes it's overloaded Equals operator it can't cast BaseValidator as SingletonTypeWrapper.

Would a simple guard clause in the Equals overload suffice here? Is there another approach I can use in our code?

Any insights would be appreciated as we can't even reliably replicate the issue right now.

Attachments
Filename File size Added on Approval
CommonEntityValidator.zip 1,208 14-Feb-2025 22:13.59 Approved
ChrisJCarlson avatar
Posts: 5
Joined: 14-Feb-2025
# Posted on: 14-Feb-2025 23:43:31   

Other potentially useful information:
We are using .net framework 4.8
We are using LLBLGen Pro version 5.6
I've upload CommonEntityValidator.zip which contains a .cs file with BaseValidator in it.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39825
Joined: 17-Aug-2003
# Posted on: 15-Feb-2025 09:54:03   

I can't reproduce it (using 5.11), but that doesn't mean there's not an issue. The way it works is that it serializes an object that's the same just once and after that writes a token that represents the same object (hence the hashtable).

So the Equals call to the hashtable, it apparently means that there's a hashcode collision and the hashtable falls back to equals comparisons. As the objects are added as-is, it uses the Object.GetHashCode which produces an integer which should be pretty unique, BUT as it's an integer, it can be it's colliding with a hashcode that's the same as another object. But this is extremely rare AFAIK. However not impossible. (which might be the reason it works most of the time).

So when this happens, when the hashcode of objects is the same the hashtable has to rely on Equals comparisons which then runs into your issue, I think. At least that's what I can think of.

In the almost 20 years this code is in production you're the first to report this exception, so it's a bit odd you run into this and no-one else. The validator doesn't do weird things either so it should work ok.

That said, the implementation in the runtime isn't the best, a guard clause has to be added to the Equals function to check the types before the cast. 5.6 isn't in support anymore tho.

Frans Bouma | Lead developer LLBLGen Pro
ChrisJCarlson avatar
Posts: 5
Joined: 14-Feb-2025
# Posted on: 16-Feb-2025 03:54:14   

Thank you looking into this so quickly. We are due for upgrading our version. Can a patch be applied so we can upgrade and likely resolve this issue? I imagine it would be for 5.11.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39825
Joined: 17-Aug-2003
# Posted on: 17-Feb-2025 10:34:14   

We've added a guardclause to the Equals call in the hotfix we just released v5.11.5 simple_smile

Frans Bouma | Lead developer LLBLGen Pro
ChrisJCarlson avatar
Posts: 5
Joined: 14-Feb-2025
# Posted on: 17-Feb-2025 14:27:38   

Thank you.

ChrisJCarlson avatar
Posts: 5
Joined: 14-Feb-2025
# Posted on: 17-Feb-2025 16:30:31   

I'm not seeing the NPM package available. I assume it will take some time before it is generally available?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39825
Joined: 17-Aug-2003
# Posted on: 18-Feb-2025 08:00:35   

ChrisJCarlson wrote:

I'm not seeing the NPM package available. I assume it will take some time before it is generally available?

It's available, but as it's a hotfix, you have to enable pre-releases https://www.nuget.org/packages/SD.LLBLGen.Pro.ORMSupportClasses/5.11.5-hotfix-20250217

Frans Bouma | Lead developer LLBLGen Pro