IGNITE-14293 .NET: Fix AffinityKey metadata in QueryEntity.KeyType

`AffinityKey` system type, when used as a `QueryEntiry` key, got overwritten by `UnmanagedCallbacks.BinaryTypeGet` call, which broke the mapping to a corresponding Java type.

* Add missing check for existing registered type (actual fix in Marshaller.cs)
* Add check for system type overwrite
* Add test for `AffinityKey` + `QueryEntity`

Co-authored-by: Igor Sapego <isapego@apache.org>
(cherry picked from commit 8cc19b1041b64d7dd3bb368ce4b9cadee07a3718)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs
index 37bc53b..cd7a327 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs
@@ -122,6 +122,46 @@
         }
 
         /// <summary>
+        /// Tests that <see cref="AffinityKey"/> works when used as <see cref="QueryEntity.KeyType"/>.
+        /// </summary>
+        [Test]
+        public void TestAffinityKeyWithQueryEntity()
+        {
+            var cacheCfg = new CacheConfiguration(TestUtils.TestName)
+            {
+                QueryEntities = new List<QueryEntity>
+                {
+                    new QueryEntity(typeof(AffinityKey), typeof(QueryEntityValue))
+                }
+            };
+
+            var ignite = Ignition.GetIgnite("grid-0");
+            var cache = ignite.GetOrCreateCache<AffinityKey, QueryEntityValue>(cacheCfg);
+            var aff = ignite.GetAffinity(cache.Name);
+
+            var ignite2 = Ignition.GetIgnite("grid-1");
+            var cache2 = ignite2.GetOrCreateCache<AffinityKey, QueryEntityValue>(cacheCfg);
+            var aff2 = ignite2.GetAffinity(cache2.Name);
+
+            // Check mapping.
+            for (var i = 0; i < 100; i++)
+            {
+                Assert.AreEqual(aff.GetPartition(i), aff.GetPartition(new AffinityKey("foo" + i, i)));
+                Assert.AreEqual(aff2.GetPartition(i), aff2.GetPartition(new AffinityKey("bar" + i, i)));
+                Assert.AreEqual(aff.GetPartition(i), aff2.GetPartition(i));
+            }
+
+            // Check put/get.
+            var key = new AffinityKey("x", 123);
+            var expected = new QueryEntityValue {Name = "y", AffKey = 321};
+            cache[key] = expected;
+
+            var val = cache2[key];
+            Assert.AreEqual(expected.Name, val.Name);
+            Assert.AreEqual(expected.AffKey, val.AffKey);
+        }
+
+        /// <summary>
         /// Tests that <see cref="AffinityKeyMappedAttribute"/> works when used on a property of a type that is
         /// specified as <see cref="QueryEntity.KeyType"/> or <see cref="QueryEntity.ValueType"/>.
         /// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/Marshaller.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/Marshaller.cs
index f6346a5..a7c5411 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/Marshaller.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Binary/Marshaller.cs
@@ -20,6 +20,7 @@
     using System;
     using System.Collections.Generic;
     using System.Diagnostics;
+    using System.Diagnostics.CodeAnalysis;
     using System.Linq;
     using System.Runtime.Serialization;
     using System.Threading;
@@ -565,6 +566,11 @@
 
                 if (type != null)
                 {
+                    if (_typeToDesc.TryGetValue(type, out desc))
+                    {
+                        return desc;
+                    }
+
                     return AddUserType(type, typeId, GetTypeName(type), true, desc);
                 }
             }
@@ -665,12 +671,26 @@
                 ThrowConflictingTypeError(type, desc0.Type, typeId);
             }
 
+            ValidateRegistration(type);
             _typeToDesc.Set(type, desc);
 
             return desc;
         }
 
         /// <summary>
+        /// Validates type registration.
+        /// </summary>
+        [ExcludeFromCodeCoverage]
+        private void ValidateRegistration(Type type)
+        {
+            BinaryFullTypeDescriptor desc;
+            if (_typeToDesc.TryGetValue(type, out desc) && !desc.UserType)
+            {
+                throw new BinaryObjectException("Invalid attempt to overwrite system type registration: " + type);
+            }
+        }
+
+        /// <summary>
         /// Throws the conflicting type error.
         /// </summary>
         private static void ThrowConflictingTypeError(object type1, object type2, int typeId)
@@ -805,6 +825,7 @@
 
             if (type != null)
             {
+                ValidateRegistration(type);
                 _typeToDesc.Set(type, descriptor);
             }