This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 895502ecd1 IGNITE-17314 .NET: Implement unified exception handling 
(#1074)
895502ecd1 is described below

commit 895502ecd188189a283a9fea37bac86a68d27b12
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon Sep 19 08:24:13 2022 +0300

    IGNITE-17314 .NET: Implement unified exception handling (#1074)
    
    * Add all exception groups, codes, and classes from the Java side.
    * When server returns an error, throw corresponding mapped .NET exception 
type.
    * Use [Source 
Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)
 to produce C# code for error groups and exception classes. When there is a new 
error code or exception added on Java side, no changes are required on C# side, 
only a rebuild.
---
 .../Apache.Ignite.Internal.Generators.csproj       |  41 +++++
 .../ErrorGroupsGenerator.cs                        | 153 ++++++++++++++++
 .../ExceptionTemplate.cs                           |  60 +++++++
 .../ExceptionsGenerator.cs                         | 177 +++++++++++++++++++
 .../GeneratorUtils.cs                              |  37 ++++
 .../platforms/dotnet/Apache.Ignite.Tests.ruleset   |   3 +
 .../Apache.Ignite.Tests/ClientSocketTests.cs       |   2 +-
 .../Compute/ComputeClusterAwarenessTests.cs        |   2 +-
 .../Apache.Ignite.Tests/Compute/ComputeTests.cs    |  15 +-
 .../dotnet/Apache.Ignite.Tests/ErrorGroupTests.cs  | 196 +++++++++++++++++++++
 .../dotnet/Apache.Ignite.Tests/ExceptionsTests.cs  | 101 +++++++++++
 .../dotnet/Apache.Ignite.Tests/FakeServer.cs       |   4 +-
 .../dotnet/Apache.Ignite.Tests/FakeServerTests.cs  |   8 +-
 .../Apache.Ignite.Tests/ProjectFilesTests.cs       |   1 +
 .../dotnet/Apache.Ignite.Tests/RetryPolicyTests.cs |  16 +-
 .../dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs     |  41 ++++-
 .../dotnet/Apache.Ignite.Tests/StringExtensions.cs |  30 ++++
 .../Apache.Ignite.Tests/Table/IgniteTupleTests.cs  |   8 +-
 .../Table/RecordViewBinaryTests.cs                 |   2 +-
 .../Table/RecordViewPocoTests.cs                   |   2 +-
 modules/platforms/dotnet/Apache.Ignite.sln         |   6 +
 .../dotnet/Apache.Ignite/Apache.Ignite.csproj      |   7 +
 .../platforms/dotnet/Apache.Ignite/ErrorGroups.cs  |  68 +++++++
 .../IgniteClientConnectionException.cs             |  70 ++++++++
 .../dotnet/Apache.Ignite/IgniteClientException.cs  |  80 +++------
 .../dotnet/Apache.Ignite/IgniteException.cs        |  97 ++++++++++
 .../Apache.Ignite/Internal/ClientFailoverSocket.cs |  11 +-
 .../dotnet/Apache.Ignite/Internal/ClientSocket.cs  |  28 +--
 .../Apache.Ignite/Internal/Compute/Compute.cs      |   5 +-
 .../dotnet/Apache.Ignite/Internal/Endpoint.cs      |   7 +-
 .../Proto/BinaryTuple/BinaryTupleBuilder.cs        |   2 +-
 .../Proto/BinaryTuple/BinaryTupleReader.cs         |   2 +-
 .../Internal/Proto/MessagePackReaderExtensions.cs  |   4 +-
 .../Internal/Proto/MessagePackWriterExtensions.cs  |   4 +-
 .../dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs |  10 +-
 .../Table/Serialization/BinaryTupleMethods.cs      |   4 +-
 .../Table/Serialization/ObjectSerializerHandler.cs |   7 +-
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   |   2 +-
 .../dotnet/Apache.Ignite/Sql/SqlStatement.cs       |   2 +-
 .../dotnet/Apache.Ignite/Table/IgniteTuple.cs      |  12 +-
 modules/platforms/dotnet/DEVNOTES.md               |   3 +-
 modules/platforms/dotnet/Directory.Build.props     |   2 +-
 42 files changed, 1204 insertions(+), 128 deletions(-)

diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/Apache.Ignite.Internal.Generators.csproj
 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/Apache.Ignite.Internal.Generators.csproj
new file mode 100644
index 0000000000..e5f11d2788
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/Apache.Ignite.Internal.Generators.csproj
@@ -0,0 +1,41 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <Nullable>enable</Nullable>
+        
<CodeAnalysisRuleSet>..\Apache.Ignite.Tests.ruleset</CodeAnalysisRuleSet>
+        <IsPackable>false</IsPackable>
+    </PropertyGroup>
+    
+    <ItemGroup>
+        <!-- Source Generators -->
+        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" 
Version="3.11.0" PrivateAssets="all" />
+        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" 
Version="3.3.3">
+            <PrivateAssets>all</PrivateAssets>
+            <IncludeAssets>runtime; build; native; contentfiles; analyzers; 
buildtransitive</IncludeAssets>
+        </PackageReference>
+    </ItemGroup>
+    
+    <ItemGroup>
+      <Compile Remove="ExceptionTemplate.cs" />
+      <EmbeddedResource Include="ExceptionTemplate.cs" />
+    </ItemGroup>
+
+</Project>
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ErrorGroupsGenerator.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ErrorGroupsGenerator.cs
new file mode 100644
index 0000000000..764ecc3cd5
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ErrorGroupsGenerator.cs
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Internal.Generators
+{
+    using System;
+    using System.Globalization;
+    using System.IO;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text;
+    using System.Text.RegularExpressions;
+    using Microsoft.CodeAnalysis;
+
+    /// <summary>
+    /// Generates error groups source from ErrorGroups.java.
+    /// </summary>
+    [Generator]
+    public sealed class ErrorGroupsGenerator : ISourceGenerator
+    {
+        /// <inheritdoc/>
+        public void Initialize(GeneratorInitializationContext context)
+        {
+            // No-op.
+        }
+
+        /// <inheritdoc/>
+        public void Execute(GeneratorExecutionContext context)
+        {
+            var javaErrorGroupsFile = Path.GetFullPath(Path.Combine(
+                context.GetJavaModulesDirectory(),
+                "core",
+                "src",
+                "main",
+                "java",
+                "org",
+                "apache",
+                "ignite",
+                "lang",
+                "ErrorGroups.java"));
+
+            if (!File.Exists(javaErrorGroupsFile))
+            {
+                throw new Exception("File not found: " + javaErrorGroupsFile + 
" = " + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!);
+            }
+
+            var javaErrorGroupsText = File.ReadAllText(javaErrorGroupsFile);
+
+            // ErrorGroup TX_ERR_GROUP = ErrorGroup.newGroup("TX", 7);
+            var javaErrorGroups = Regex.Matches(
+                    javaErrorGroupsText,
+                    @"public static class ([A-Za-z]+) {\s+/\*\*.*?\*/\s+public 
static final ErrorGroup ([\w_]+)_ERR_GROUP = ErrorGroup.newGroup\(""([A-Z]+)"", 
(\d+)",
+                    RegexOptions.Singleline | RegexOptions.CultureInvariant)
+                .Cast<Match>()
+                .Select(x => (ClassName: x.Groups[1].Value, GroupName: 
x.Groups[2].Value, ShortGroupName: x.Groups[3].Value, Code: 
int.Parse(x.Groups[4].Value, CultureInfo.InvariantCulture)))
+                .ToList();
+
+            if (javaErrorGroups.Count == 0)
+            {
+                throw new Exception($"Failed to parse Java error groups from 
'{javaErrorGroupsFile}'");
+            }
+
+            var sb = new StringBuilder();
+
+            sb.AppendLine("// <auto-generated/>");
+            sb.AppendLine("namespace Apache.Ignite");
+            sb.AppendLine("{");
+            sb.AppendLine("    using System;\n");
+            sb.AppendLine("    public static partial class ErrorGroups");
+            sb.AppendLine("    {");
+
+            // GetGroupName.
+            sb.AppendLine(
+@"        /// <summary>
+        /// Gets the group name by code.
+        /// </summary>
+        /// <param name=""groupCode"">Group code.</param>
+        /// <returns>Group name.</returns>
+        public static string GetGroupName(int groupCode) => groupCode switch
+        {");
+
+            foreach (var (className, _, _, _) in javaErrorGroups)
+            {
+                sb.AppendLine(@$"            {className}.GroupCode => 
{className}.GroupName,");
+            }
+
+            sb.AppendLine(
+                @"
+            _ => UnknownGroupName
+        };");
+
+            // Groups.
+            foreach (var (className, groupName, shortGroupName, groupCode) in 
javaErrorGroups)
+            {
+                sb.AppendLine();
+                sb.AppendLine($"        /// <summary> {className} errors. 
</summary>");
+                sb.AppendLine($"        public static class {className}");
+                sb.AppendLine($"        {{");
+                sb.AppendLine($"            /// <summary> {className} group 
code. </summary>");
+                sb.AppendLine($"            public const int GroupCode = 
{groupCode};");
+                sb.AppendLine();
+                sb.AppendLine($"            /// <summary> {className} group 
name. </summary>");
+                sb.AppendLine($"            public const string GroupName = 
\"{shortGroupName}\";");
+
+                // TX_STATE_STORAGE_CREATE_ERR = 
TX_ERR_GROUP.registerErrorCode(1)
+                var javaErrors = Regex.Matches(
+                        javaErrorGroupsText,
+                        @"([\w_]+)_ERR = " + groupName + 
@"_ERR_GROUP\.registerErrorCode\((\d+)\);")
+                    .Cast<Match>()
+                    .Select(x => (Name: x.Groups[1].Value, Code: 
int.Parse(x.Groups[2].Value, CultureInfo.InvariantCulture)))
+                    .ToList();
+
+                if (javaErrors.Count == 0)
+                {
+                    throw new Exception($"Failed to parse Java errors for 
group {groupName} from '{javaErrorGroupsFile}'");
+                }
+
+                foreach (var (errorName, errorCode) in javaErrors)
+                {
+                    var dotNetErrorName = SnakeToCamelCase(errorName);
+
+                    sb.AppendLine();
+                    sb.AppendLine($"            /// <summary> 
{dotNetErrorName} error. </summary>");
+                    sb.AppendLine($"            public static readonly int 
{dotNetErrorName} = GetFullCode(GroupCode, {errorCode});");
+                }
+
+                sb.AppendLine("        }");
+            }
+
+            sb.AppendLine("    }");
+            sb.AppendLine("}");
+
+            context.AddSource("ErrorGroups.g.cs", sb.ToString());
+        }
+
+        private static string SnakeToCamelCase(string str) =>
+            string.Concat(str.Split('_').Select(x => x.Substring(0, 
1).ToUpperInvariant() + x.Substring(1).ToLowerInvariant()));
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ExceptionTemplate.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ExceptionTemplate.cs
new file mode 100644
index 0000000000..b9b5088143
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ExceptionTemplate.cs
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// <auto-generated/>
+#nullable enable
+namespace NAMESPACE
+{
+    using System;
+    using System.Diagnostics.CodeAnalysis;
+    using System.Runtime.Serialization;
+
+    /// <summary>
+    /// XMLDOC
+    /// </summary>
+    [Serializable]
+    [SuppressMessage(
+        "Microsoft.Design",
+        "CA1032:ImplementStandardExceptionConstructors",
+        Justification="Ignite exceptions use a special constructor.")]
+    public sealed class IgniteTemplateException : IgniteException
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="IgniteTemplateException"/> class.
+        /// </summary>
+        /// <param name="traceId">Trace id.</param>
+        /// <param name="code">Code.</param>
+        /// <param name="message">Message.</param>
+        /// <param name="innerException">Inner exception.</param>
+        public IgniteTemplateException(Guid traceId, int code, string? 
message, Exception? innerException = null)
+            : base(traceId, code, message, innerException)
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="IgniteTemplateException"/> class.
+        /// </summary>
+        /// <param name="serializationInfo">Serialization information.</param>
+        /// <param name="streamingContext">Streaming context.</param>
+        private IgniteTemplateException(SerializationInfo serializationInfo, 
StreamingContext streamingContext)
+            : base(serializationInfo, streamingContext)
+        {
+            // No-op.
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ExceptionsGenerator.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ExceptionsGenerator.cs
new file mode 100644
index 0000000000..0bd353f5a0
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/ExceptionsGenerator.cs
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Internal.Generators
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Collections.Immutable;
+    using System.IO;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text;
+    using System.Text.RegularExpressions;
+    using Microsoft.CodeAnalysis;
+
+    /// <summary>
+    /// Generates exception classes from Java exceptions.
+    /// </summary>
+    [Generator]
+    public sealed class ExceptionsGenerator : ISourceGenerator
+    {
+        /// <inheritdoc/>
+        public void Initialize(GeneratorInitializationContext context)
+        {
+            // No-op.
+        }
+
+        /// <inheritdoc/>
+        public void Execute(GeneratorExecutionContext context)
+        {
+            var javaModulesDirectory = context.GetJavaModulesDirectory();
+
+            var javaExceptionsWithParents = Directory.EnumerateFiles(
+                    javaModulesDirectory,
+                    "*Exception.java",
+                    SearchOption.AllDirectories)
+                .Where(x => !x.Contains("internal"))
+                .Select(File.ReadAllText)
+                .Select(x => (
+                    Class: Regex.Match(x, @"public class (\w+) extends (\w+)"),
+                    Source: x))
+                .Where(x => x.Class.Success)
+                .Where(x => !x.Class.Value.Contains("RaftException")) // 
Ignore duplicate RaftException.
+                .Where(x => !x.Class.Value.Contains("IgniteClient")) // Skip 
Java client exceptions.
+                .ToDictionary(x => x.Class.Groups[1].Value, x => (Parent: 
x.Class.Groups[2].Value, x.Source));
+
+            var existingExceptions = context.Compilation.SyntaxTrees
+                .Where(x => x.FilePath.Contains("Exception"))
+                .Select(x => Path.GetFileNameWithoutExtension(x.FilePath))
+                .ToImmutableHashSet();
+
+            var javaExceptions = javaExceptionsWithParents
+                .Where(x => !existingExceptions.Contains(x.Key))
+                .Where(x => IsIgniteException(x.Key))
+                .ToList();
+
+            if (javaExceptionsWithParents.Count == 0 || javaExceptions.Count 
== 0)
+            {
+                throw new Exception($"Failed to detect Java exception classes 
in {javaModulesDirectory}.");
+            }
+
+            var template = GetExceptionClassTemplate();
+
+            var classMap = new List<(string JavaClass, string DotNetClass)>();
+
+            foreach (var javaException in javaExceptions)
+            {
+                var className = javaException.Key;
+
+                var (javaPackage, dotNetNamespace) = 
GetPackageAndNamespace(className, javaException.Value.Source);
+
+                var src = template
+                    .Replace("IgniteTemplateException", className)
+                    .Replace("XMLDOC", GetXmlDoc(className, 
javaException.Value.Source))
+                    .Replace("NAMESPACE", dotNetNamespace);
+
+                context.AddSource(className + ".g.cs", src);
+
+                classMap.Add((javaPackage + "." + className, dotNetNamespace + 
"." + className));
+            }
+
+            EmitClassMap(context, classMap);
+
+            bool IsIgniteException(string? ex) =>
+                ex != null &&
+                (ex == "IgniteException" ||
+                 IsIgniteException(javaExceptionsWithParents.TryGetValue(ex, 
out var parent) ? parent.Parent : null));
+        }
+
+        private static void EmitClassMap(GeneratorExecutionContext context, 
List<(string JavaClass, string DotNetClass)> classMap)
+        {
+            var sb = new StringBuilder();
+
+            sb.AppendLine("// <auto-generated/>");
+            sb.AppendLine("#nullable enable");
+            sb.AppendLine("namespace Apache.Ignite.Internal");
+            sb.AppendLine("{");
+            sb.AppendLine("    using System;");
+            sb.AppendLine();
+            sb.AppendLine("    internal static class ExceptionMapper");
+            sb.AppendLine("    {");
+            sb.AppendLine("        public static IgniteException 
GetException(Guid traceId, int code, string javaClass, string? message) =>");
+            sb.AppendLine("            javaClass switch");
+            sb.AppendLine("            {");
+
+            foreach (var (javaClass, dotNetClass) in classMap)
+            {
+                sb.AppendLine($"                \"{javaClass}\" => new 
{dotNetClass}(traceId, code, message, new IgniteException(traceId, code, 
javaClass)),");
+            }
+
+            sb.AppendLine("                _ => new IgniteException(traceId, 
code, message, new IgniteException(traceId, code, javaClass))");
+            sb.AppendLine("            };");
+            sb.AppendLine("    }");
+            sb.AppendLine("}");
+
+            context.AddSource("ExceptionMapper.g.cs", sb.ToString());
+        }
+
+        private static string GetXmlDoc(string javaClassName, string 
javaSource)
+        {
+            var javaDocMatch = Regex.Match(javaSource, 
@"/\*\*\s*\*?\s*(.*?)\s*\*/\s+public class", RegexOptions.Singleline);
+
+            if (!javaDocMatch.Success)
+            {
+                throw new Exception($"Failed to parse Java package name from 
'{javaClassName}.java'");
+            }
+
+            var xmlDoc = javaDocMatch.Groups[1].Value
+                .Replace(Environment.NewLine, " ")
+                .Replace('\n', ' ')
+                .Replace(" * ", " ");
+
+            return xmlDoc;
+        }
+
+        private static (string JavaPackage, string DotNetNamespace) 
GetPackageAndNamespace(string javaClassName, string javaSource)
+        {
+            var javaPackageMatch = Regex.Match(javaSource, @"package 
org\.apache(\.[a-z.]+);");
+
+            if (!javaPackageMatch.Success)
+            {
+                throw new Exception($"Failed to parse Java package name from 
'{javaClassName}.java'");
+            }
+
+            var javaPackage = javaPackageMatch.Groups[1].Value;
+
+            var ns = Regex.Replace(javaPackage, @"(\.[a-z])", x => 
x.Groups[1].Value.ToUpperInvariant())
+                .Replace(".Lang", string.Empty);
+
+            return ("org.apache" + javaPackage, "Apache" + ns);
+        }
+
+        private static string GetExceptionClassTemplate()
+        {
+            using var stream = 
Assembly.GetExecutingAssembly().GetManifestResourceStream(
+                "Apache.Ignite.Internal.Generators.ExceptionTemplate.cs");
+
+            using var reader = new StreamReader(stream!);
+
+            return reader.ReadToEnd();
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/GeneratorUtils.cs 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/GeneratorUtils.cs
new file mode 100644
index 0000000000..8e95f6f4fc
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Internal.Generators/GeneratorUtils.cs
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Internal.Generators
+{
+    using System;
+    using System.IO;
+    using System.Linq;
+    using Microsoft.CodeAnalysis;
+
+    /// <summary>
+    /// Source generator utils.
+    /// </summary>
+    internal static class GeneratorUtils
+    {
+        public static string GetProjectRootDirectory(this 
GeneratorExecutionContext context) =>
+            Path.GetDirectoryName(
+                context.Compilation.SyntaxTrees.Single(x => 
x.FilePath.EndsWith("IIgnite.cs", StringComparison.Ordinal)).FilePath)!;
+
+        public static string GetJavaModulesDirectory(this 
GeneratorExecutionContext context) =>
+            Path.GetFullPath(Path.Combine(context.GetProjectRootDirectory(), 
"..", "..", ".."));
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests.ruleset 
b/modules/platforms/dotnet/Apache.Ignite.Tests.ruleset
index a95763d3d5..921a068e9e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests.ruleset
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests.ruleset
@@ -55,5 +55,8 @@
 
     <!-- Validate parameters. -->
     <Rule Id="CA1062" Action="None" />
+
+    <!-- Normalize to upper case -->
+    <Rule Id="CA1308" Action="None" />
   </Rules>
 </RuleSet>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
index e845884025..18deb81586 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs
@@ -59,7 +59,7 @@ namespace Apache.Ignite.Tests
             using var requestWriter = ProtoCommon.GetMessageWriter();
             requestWriter.GetMessageWriter().Write(123);
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(
+            var ex = Assert.ThrowsAsync<IgniteException>(
                 async () => await socket.DoOutInOpAsync((ClientOp)1234567, 
requestWriter));
 
             StringAssert.Contains("Unexpected operation code: 1234567", 
ex!.Message);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs
index 8c798f7bc5..24220b2d40 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs
@@ -42,7 +42,7 @@ namespace Apache.Ignite.Tests.Compute
             using var client = await IgniteClient.StartAsync(clientCfg);
 
             // ReSharper disable once AccessToDisposedClosure
-            TestUtils.WaitForCondition(() => client.GetConnections().Count == 
3);
+            TestUtils.WaitForCondition(() => client.GetConnections().Count == 
3, 5000);
 
             var res2 = await client.Compute.ExecuteAsync<string>(nodes: new[] 
{ server2.Node }, jobClassName: string.Empty);
             var res3 = await client.Compute.ExecuteAsync<string>(nodes: new[] 
{ server3.Node }, jobClassName: string.Empty);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
index 795cbccde1..2d6d728dc7 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
@@ -133,10 +133,19 @@ namespace Apache.Ignite.Tests.Compute
         [Test]
         public void TestJobErrorPropagatesToClientWithClassAndMessage()
         {
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () =>
+            var ex = Assert.ThrowsAsync<IgniteException>(async () =>
                 await Client.Compute.ExecuteAsync<string>(await 
Client.GetClusterNodesAsync(), ErrorJob, "unused"));
 
             StringAssert.Contains("Custom job error", ex!.Message);
+
+            Assert.AreEqual(
+                
"org.apache.ignite.internal.runner.app.client.ItThinClientComputeTest$CustomException",
+                ex.InnerException!.Message);
+
+            Assert.AreEqual(ErrorGroups.Table.ColumnAlreadyExists, ex.Code);
+            Assert.AreEqual("IGN-TBL-3", ex.CodeAsString);
+            Assert.AreEqual(3, ex.ErrorCode);
+            Assert.AreEqual("TBL", ex.GroupName);
         }
 
         [Test]
@@ -144,7 +153,7 @@ namespace Apache.Ignite.Tests.Compute
         {
             var unknownNode = new ClusterNode("x", "y", new 
IPEndPoint(IPAddress.Loopback, 0));
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () =>
+            var ex = Assert.ThrowsAsync<IgniteException>(async () =>
                 await Client.Compute.ExecuteAsync<string>(new[] { unknownNode 
}, EchoJob, "unused"));
 
             StringAssert.Contains("Specified node is not present in the 
cluster: y", ex!.Message);
@@ -215,7 +224,7 @@ namespace Apache.Ignite.Tests.Compute
         [Test]
         public void TestExecuteColocatedThrowsWhenKeyColumnIsMissing()
         {
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () =>
+            var ex = Assert.ThrowsAsync<IgniteException>(async () =>
                 await Client.Compute.ExecuteColocatedAsync<string>(TableName, 
new IgniteTuple(), EchoJob));
 
             StringAssert.Contains("Missed key column: KEY", ex!.Message);
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ErrorGroupTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/ErrorGroupTests.cs
new file mode 100644
index 0000000000..247100ec89
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ErrorGroupTests.cs
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests
+{
+    using System.Collections.Generic;
+    using System.Globalization;
+    using System.IO;
+    using System.Linq;
+    using System.Text.RegularExpressions;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="ErrorGroups"/>.
+    /// </summary>
+    public class ErrorGroupTests
+    {
+        private static readonly string JavaErrorGroupsFile = Path.Combine(
+            TestUtils.RepoRootDir, "modules", "core", "src", "main", "java", 
"org", "apache", "ignite", "lang", "ErrorGroups.java");
+
+        [Test]
+        public void TestErrorGroupCodesAreUnique()
+        {
+            var existingCodes = new Dictionary<int, string>();
+
+            foreach (var (code, name) in GetErrorGroups())
+            {
+                if (existingCodes.TryGetValue(code, out var existingGroupName))
+                {
+                    Assert.Fail($"Duplicate group code: {code} 
({existingGroupName} and {name})");
+                }
+
+                existingCodes.Add(code, name);
+            }
+        }
+
+        [Test]
+        public void TestGetGroupNameReturnsUniqueNames()
+        {
+            var existingNames = new Dictionary<string, string>();
+
+            foreach (var (code, className) in GetErrorGroups())
+            {
+                var name = ErrorGroups.GetGroupName(code);
+
+                if (existingNames.TryGetValue(name, out var existingClassName))
+                {
+                    Assert.Fail($"Duplicate group name: {name} 
({existingClassName} and {className})");
+                }
+
+                existingNames.Add(name, className);
+            }
+        }
+
+        [Test]
+        public void TestErrorCodesAreUnique()
+        {
+            var duplicateCodes = GetErrorCodes()
+                .GroupBy(x => x.Code)
+                .Select(x => x.ToList())
+                .Where(x => x.Count > 1)
+                .ToList();
+
+            Assert.Multiple(() => duplicateCodes.ForEach(
+                x => Assert.Fail($"Duplicate error code: {x[0].Code} 
({string.Join(", ", x.Select(y => y.Name))})")));
+        }
+
+        [Test]
+        public void TestErrorCodeMatchesParentGroup()
+        {
+            foreach (var (code, group, name) in GetErrorCodes())
+            {
+                var expectedGroup = ErrorGroups.GetGroupCode(code);
+
+                Assert.AreEqual(expectedGroup, group, $"Code {code} ({name}) 
has incorrect group. Expected {expectedGroup}, got {group}.");
+            }
+        }
+
+        [Test]
+        public void TestJavaErrorGroupsAndCodesHaveDotNetCounterparts()
+        {
+            var dotNetErrorCodes = GetErrorCodes().ToDictionary(x => x.Code, x 
=> (x.Name, x.GroupCode));
+
+            var javaErrorGroupsText = File.ReadAllText(JavaErrorGroupsFile);
+
+            // ErrorGroup TX_ERR_GROUP = ErrorGroup.newGroup("TX", 7);
+            var javaErrorGroups = Regex.Matches(
+                javaErrorGroupsText,
+                @"ErrorGroup ([\w_]+)_ERR_GROUP = 
ErrorGroup.newGroup\(""(\w+)"", (\d+)\);")
+                .Select(x => (Name: x.Groups[1].Value, ShortName: 
x.Groups[2].Value, Code: int.Parse(x.Groups[3].Value, 
CultureInfo.InvariantCulture)))
+                .ToList();
+
+            Assert.GreaterOrEqual(javaErrorGroups.Count, 7);
+
+            foreach (var (grpName, grpShortName, grpCode) in javaErrorGroups)
+            {
+                var dotNetName = ErrorGroups.GetGroupName(grpCode);
+
+                Assert.AreEqual(grpShortName, dotNetName, $"Java and .NET 
error group '{grpName}' names do not match");
+
+                // TX_STATE_STORAGE_CREATE_ERR = 
TX_ERR_GROUP.registerErrorCode(1)
+                var javaErrors = Regex.Matches(
+                        javaErrorGroupsText,
+                        @"([\w_]+) = " + grpName + 
@"_ERR_GROUP\.registerErrorCode\((\d+)\);")
+                    .Select(x => (Name: x.Groups[1].Value, Code: 
int.Parse(x.Groups[2].Value, CultureInfo.InvariantCulture)))
+                    .ToList();
+
+                Assert.IsNotEmpty(javaErrors);
+
+                foreach (var (errName, errCode) in javaErrors)
+                {
+                    var fullErrCode = ErrorGroups.GetFullCode(grpCode, 
errCode);
+                    var expectedDotNetName = errName.SnakeToCamelCase()[..^3];
+
+                    if (!dotNetErrorCodes.TryGetValue(fullErrCode, out var 
dotNetError))
+                    {
+                        Assert.Fail(
+                            $"Java error '{errName}' ('{errCode}') in group 
'{grpName}' ('{grpCode}') has no .NET counterpart.\n" +
+                            $"public static readonly int {expectedDotNetName} 
= GetFullCode(GroupCode, {errCode});");
+                    }
+
+                    Assert.AreEqual(grpCode, dotNetError.GroupCode);
+                    Assert.AreEqual(expectedDotNetName, dotNetError.Name);
+                }
+            }
+        }
+
+        [Test]
+        public void TestGeneratedErrorGroups()
+        {
+            Assert.AreEqual(1, ErrorGroups.Common.GroupCode);
+            Assert.AreEqual("CMN", ErrorGroups.Common.GroupName);
+            Assert.AreEqual("CMN", ErrorGroups.GetGroupName(1));
+
+            Assert.AreEqual(2, ErrorGroups.Table.GroupCode);
+            Assert.AreEqual("TBL", ErrorGroups.Table.GroupName);
+            Assert.AreEqual("TBL", ErrorGroups.GetGroupName(2));
+
+            Assert.AreEqual(3, ErrorGroups.Client.GroupCode);
+            Assert.AreEqual("CLIENT", ErrorGroups.Client.GroupName);
+            Assert.AreEqual("CLIENT", ErrorGroups.GetGroupName(3));
+
+            Assert.AreEqual(4, ErrorGroups.Sql.GroupCode);
+            Assert.AreEqual("SQL", ErrorGroups.Sql.GroupName);
+            Assert.AreEqual("SQL", ErrorGroups.GetGroupName(4));
+
+            Assert.AreEqual(5, ErrorGroups.MetaStorage.GroupCode);
+            Assert.AreEqual("META", ErrorGroups.MetaStorage.GroupName);
+            Assert.AreEqual("META", ErrorGroups.GetGroupName(5));
+
+            Assert.AreEqual(6, ErrorGroups.Index.GroupCode);
+            Assert.AreEqual("IDX", ErrorGroups.Index.GroupName);
+            Assert.AreEqual("IDX", ErrorGroups.GetGroupName(6));
+
+            Assert.AreEqual(7, ErrorGroups.Transactions.GroupCode);
+            Assert.AreEqual("TX", ErrorGroups.Transactions.GroupName);
+            Assert.AreEqual("TX", ErrorGroups.GetGroupName(7));
+        }
+
+        [Test]
+        public void TestGetGroupNameForUnknownGroupCode()
+        {
+            // Newer servers may return unknown to us error codes.
+            Assert.AreEqual("UNKNOWN", ErrorGroups.GetGroupName(-1));
+            Assert.AreEqual(ErrorGroups.UnknownGroupName, 
ErrorGroups.GetGroupName(9999));
+        }
+
+        private static IEnumerable<(int Code, string Name)> GetErrorGroups() 
=> typeof(ErrorGroups).GetNestedTypes()
+                .Select(x => ((int) x.GetField("GroupCode")!.GetValue(null)!, 
x.Name));
+
+        private static IEnumerable<(int Code, int GroupCode, string Name)> 
GetErrorCodes() => typeof(ErrorGroups).GetNestedTypes()
+                .SelectMany(groupClass =>
+                {
+                    var groupCode = 
(int)groupClass.GetField("GroupCode")!.GetValue(null)!;
+
+                    return groupClass
+                        .GetFields()
+                        .Where(x => x.Name != "GroupCode" && x.Name != 
"GroupName")
+                        .Select(errCode => ((int)errCode.GetValue(null)!, 
groupCode, errCode.Name));
+                });
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ExceptionsTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/ExceptionsTests.cs
new file mode 100644
index 0000000000..ad15237cf6
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ExceptionsTests.cs
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Collections.Immutable;
+    using System.IO;
+    using System.Linq;
+    using System.Runtime.Serialization.Formatters.Binary;
+    using System.Text.RegularExpressions;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests Ignite exceptions.
+    /// </summary>
+    public class ExceptionsTests
+    {
+        [Test]
+        public void TestExceptionsAreSerializableAndHaveRequiredConstructors()
+        {
+            var types = typeof(IIgnite).Assembly.GetTypes().Where(x => 
x.IsSubclassOf(typeof(Exception)));
+
+            foreach (var type in types)
+            {
+                Assert.IsTrue(type.IsSerializable, "Exception is not 
serializable: " + type);
+
+                // Check required IgniteException constructor.
+                var defCtor = type.GetConstructor(new[] { typeof(Guid), 
typeof(int), typeof(string), typeof(Exception) });
+                Assert.IsNotNull(defCtor, "Required constructor is missing: " 
+ type);
+
+                var traceId = Guid.NewGuid();
+                var ex = (IgniteException)defCtor!.Invoke(new object[] { 
traceId, 123, "myMessage", new Exception() });
+                Assert.AreEqual("myMessage", ex.Message);
+
+                // Serialization.
+                var stream = new MemoryStream();
+                var formatter = new BinaryFormatter();
+
+                formatter.Serialize(stream, ex);
+                stream.Seek(0, SeekOrigin.Begin);
+
+                var res = (IgniteException) formatter.Deserialize(stream);
+                Assert.AreEqual("myMessage", res.Message);
+                Assert.AreEqual(traceId, res.TraceId);
+                Assert.AreEqual(123, res.Code);
+            }
+        }
+
+        [Test]
+        public void TestAllJavaIgniteExceptionsHaveDotNetCounterparts()
+        {
+            var modulesDir = 
Path.GetFullPath(Path.Combine(TestUtils.RepoRootDir, "modules"));
+
+            var javaExceptionsWithParents = 
Directory.EnumerateFiles(modulesDir, "*Exception.java", 
SearchOption.AllDirectories)
+                .Where(x => !x.Contains("internal"))
+                .Select(File.ReadAllText)
+                .Select(x => Regex.Match(x, @"public class (\w+) extends 
(\w+)"))
+                .Where(x => x.Success && !x.Value.Contains("RaftException")) 
// Ignore duplicate RaftException.
+                .Where(x => !x.Value.Contains("IgniteClient")) // Skip Java 
client exceptions.
+                .ToDictionary(x => x.Groups[1].Value, x => x.Groups[2].Value);
+
+            Assert.IsNotEmpty(javaExceptionsWithParents);
+
+            var javaExceptions = javaExceptionsWithParents.Select(x => 
x.Key).Where(IsIgniteException).ToList();
+
+            Assert.IsNotEmpty(javaExceptions);
+
+            Assert.Multiple(() =>
+            {
+                var dotNetExceptions = typeof(IIgnite).Assembly.GetTypes()
+                    .Select(t => t.Name)
+                    .Where(x => x.EndsWith("Exception", 
StringComparison.Ordinal))
+                    .ToImmutableHashSet();
+
+                foreach (var exception in javaExceptions)
+                {
+                    Assert.IsTrue(dotNetExceptions.Contains(exception), "No 
.NET equivalent for Java exception: " + exception);
+                }
+            });
+
+            bool IsIgniteException(string? ex) =>
+                ex != null && (ex == "IgniteException" || 
IsIgniteException(javaExceptionsWithParents.GetValueOrDefault(ex)));
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
index 40b4845187..07d5b145c0 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
@@ -387,8 +387,8 @@ namespace Apache.Ignite.Tests
                     using var errWriter = new PooledArrayBufferWriter();
                     var w = new MessagePackWriter(errWriter);
                     w.Write(Guid.Empty);
-                    w.Write(65537);
-                    w.Write("ErrCls: ");
+                    w.Write(262147);
+                    w.Write("org.foo.bar.BazException");
                     w.Write(Err);
                     w.WriteNil(); // Stack trace.
                     w.Flush();
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerTests.cs
index 8e85ec09da..2e0bb0f4a6 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServerTests.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Tests
 {
+    using System;
     using System.Threading.Tasks;
     using NUnit.Framework;
 
@@ -41,8 +42,11 @@ namespace Apache.Ignite.Tests
             using var server = new FakeServer();
             using var client = await server.ConnectClientAsync();
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await client.Tables.GetTableAsync("t"));
-            Assert.AreEqual("ErrCls: : Err! (65537, 
00000000-0000-0000-0000-000000000000)", ex!.Message);
+            var ex = Assert.ThrowsAsync<IgniteException>(async () => await 
client.Tables.GetTableAsync("t"));
+            Assert.AreEqual("Err!", ex!.Message);
+            Assert.AreEqual("org.foo.bar.BazException", 
ex.InnerException!.Message);
+            Assert.AreEqual(Guid.Empty, ex.TraceId);
+            Assert.AreEqual(ErrorGroups.Sql.QueryInvalid, ex.Code);
         }
 
         [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ProjectFilesTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/ProjectFilesTests.cs
index 09208a3ef2..ea74d699e5 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/ProjectFilesTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ProjectFilesTests.cs
@@ -61,6 +61,7 @@ namespace Apache.Ignite.Tests
             {
                 if (file.Contains(InternalDir, StringComparison.Ordinal) ||
                     file.Contains(".Tests", StringComparison.Ordinal) ||
+                    file.Contains(".Internal.", StringComparison.Ordinal) ||
                     file.Contains(".Benchmarks", StringComparison.Ordinal) ||
                     file.EndsWith("RetryLimitPolicy.cs", 
StringComparison.Ordinal) ||
                     file.EndsWith("Exception.cs", StringComparison.Ordinal))
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/RetryPolicyTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/RetryPolicyTests.cs
index b1febfe124..aca74abafd 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/RetryPolicyTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/RetryPolicyTests.cs
@@ -54,7 +54,7 @@ namespace Apache.Ignite.Tests
             using var server = new FakeServer(reqId => reqId % 2 == 0);
             using var client = await server.ConnectClientAsync(cfg);
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await client.Tables.GetTableAsync("bad-table"));
+            var ex = Assert.ThrowsAsync<IgniteException>(async () => await 
client.Tables.GetTableAsync("bad-table"));
             StringAssert.Contains(FakeServer.Err, ex!.Message);
         }
 
@@ -69,7 +69,7 @@ namespace Apache.Ignite.Tests
 
             var tx = await client.Transactions.BeginAsync();
 
-            Assert.ThrowsAsync<IgniteClientException>(async () => await 
tx.CommitAsync());
+            Assert.ThrowsAsync<IgniteClientConnectionException>(async () => 
await tx.CommitAsync());
             Assert.IsEmpty(testRetryPolicy.Invocations);
         }
 
@@ -86,7 +86,7 @@ namespace Apache.Ignite.Tests
 
             await client.Tables.GetTablesAsync();
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await client.Tables.GetTablesAsync());
+            var ex = Assert.ThrowsAsync<IgniteClientConnectionException>(async 
() => await client.Tables.GetTablesAsync());
             Assert.AreEqual("Operation failed after 5 retries, examine 
InnerException for details.", ex!.Message);
         }
 
@@ -98,7 +98,7 @@ namespace Apache.Ignite.Tests
 
             await client.Tables.GetTablesAsync();
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await client.Tables.GetTablesAsync());
+            var ex = Assert.ThrowsAsync<IgniteClientConnectionException>(async 
() => await client.Tables.GetTablesAsync());
             Assert.AreEqual("Operation failed after 16 retries, examine 
InnerException for details.", ex!.Message);
         }
 
@@ -130,7 +130,7 @@ namespace Apache.Ignite.Tests
 
             await client.Tables.GetTablesAsync();
 
-            Assert.ThrowsAsync<IgniteClientException>(async () => await 
client.Tables.GetTablesAsync());
+            Assert.ThrowsAsync<IgniteClientConnectionException>(async () => 
await client.Tables.GetTablesAsync());
         }
 
         [Test]
@@ -146,7 +146,7 @@ namespace Apache.Ignite.Tests
 
             await client.Tables.GetTablesAsync();
 
-            Assert.ThrowsAsync<IgniteClientException>(async () => await 
client.Tables.GetTablesAsync());
+            Assert.ThrowsAsync<IgniteClientConnectionException>(async () => 
await client.Tables.GetTablesAsync());
         }
 
         [Test]
@@ -210,7 +210,7 @@ namespace Apache.Ignite.Tests
 
             var table = await 
client.Tables.GetTableAsync(FakeServer.ExistingTableName);
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await table!.RecordBinaryView.UpsertAsync(tx, new IgniteTuple()));
+            var ex = Assert.ThrowsAsync<IgniteClientConnectionException>(async 
() => await table!.RecordBinaryView.UpsertAsync(tx, new IgniteTuple()));
             StringAssert.StartsWith("Socket is closed due to an error", 
ex!.Message);
         }
 
@@ -244,7 +244,7 @@ namespace Apache.Ignite.Tests
             using var client = await server.ConnectClientAsync(cfg);
 
             var table = await 
client.Tables.GetTableAsync(FakeServer.ExistingTableName);
-            Assert.ThrowsAsync<IgniteClientException>(async () => await 
table!.RecordBinaryView.UpsertAsync(null, new IgniteTuple()));
+            Assert.ThrowsAsync<IgniteClientConnectionException>(async () => 
await table!.RecordBinaryView.UpsertAsync(null, new IgniteTuple()));
         }
 
         private class TestRetryPolicy : RetryLimitPolicy
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
index 5d10eadc16..4517a52ae3 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
@@ -240,10 +240,49 @@ namespace Apache.Ignite.Tests.Sql
         [Test]
         public void TestInvalidSqlThrowsException()
         {
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await Client.Sql.ExecuteAsync(null, "select x from bad"));
+            var ex = Assert.ThrowsAsync<IgniteException>(async () => await 
Client.Sql.ExecuteAsync(null, "select x from bad"));
             StringAssert.Contains("From line 1, column 15 to line 1, column 
17: Object 'BAD' not found", ex!.Message);
         }
 
+        [Test]
+        public void TestCreateTableExistsThrowsException()
+        {
+            var ex = Assert.ThrowsAsync<TableAlreadyExistsException>(
+                async () => await Client.Sql.ExecuteAsync(null, "CREATE TABLE 
TEST(ID INT PRIMARY KEY)"));
+
+            StringAssert.EndsWith("Table already exists [name=PUBLIC.TEST]", 
ex!.Message);
+            StringAssert.StartsWith("IGN-TBL-1", ex.Message);
+            StringAssert.StartsWith("IGN-TBL-1", ex.CodeAsString);
+            StringAssert.StartsWith("TBL", ex.GroupName);
+            Assert.AreEqual(ErrorGroups.Table.TableAlreadyExists, ex.Code);
+        }
+
+        [Test]
+        public void TestAlterTableNotFoundThrowsException()
+        {
+            var ex = Assert.ThrowsAsync<TableNotFoundException>(
+                async () => await Client.Sql.ExecuteAsync(null, "ALTER TABLE 
NOT_EXISTS_TABLE ADD COLUMN VAL1 VARCHAR"));
+
+            StringAssert.EndsWith("Table does not exist 
[name=PUBLIC.NOT_EXISTS_TABLE]", ex!.Message);
+            StringAssert.StartsWith("IGN-TBL-2", ex.Message);
+            StringAssert.StartsWith("IGN-TBL-2", ex.CodeAsString);
+            StringAssert.StartsWith("TBL", ex.GroupName);
+            Assert.AreEqual(ErrorGroups.Table.TableNotFound, ex.Code);
+        }
+
+        [Test]
+        public void TestAlterTableColumnExistsThrowsException()
+        {
+            var ex = Assert.ThrowsAsync<ColumnAlreadyExistsException>(
+                async () => await Client.Sql.ExecuteAsync(null, "ALTER TABLE 
TEST ADD COLUMN ID INT"));
+
+            StringAssert.EndsWith("Column already exists [name=ID]", 
ex!.Message);
+            StringAssert.StartsWith("IGN-TBL-3", ex.Message);
+            StringAssert.StartsWith("IGN-TBL-3", ex.CodeAsString);
+            StringAssert.StartsWith("TBL", ex.GroupName);
+            Assert.AreEqual(ErrorGroups.Table.ColumnAlreadyExists, ex.Code);
+        }
+
         [Test]
         public async Task TestStatementProperties()
         {
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/StringExtensions.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/StringExtensions.cs
new file mode 100644
index 0000000000..10ff0c1a8c
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/StringExtensions.cs
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests
+{
+    using System.Linq;
+
+    /// <summary>
+    /// <see cref="string"/> extensions.
+    /// </summary>
+    public static class StringExtensions
+    {
+        public static string SnakeToCamelCase(this string str) =>
+            string.Concat(str.Split('_').Select(x => x[..1].ToUpperInvariant() 
+ x[1..].ToLowerInvariant()));
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
index 50756b94c1..a2eb5f6157 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
@@ -70,19 +70,19 @@ namespace Apache.Ignite.Tests.Table
         {
             var tuple = new IgniteTuple { ["Foo"] = 1 };
 
-            var ex = Assert.Throws<IgniteClientException>(() => 
tuple.GetOrdinal(string.Empty));
+            var ex = Assert.Throws<ArgumentException>(() => 
tuple.GetOrdinal(string.Empty));
             Assert.AreEqual("Column name can not be null or empty.", 
ex!.Message);
 
-            ex = Assert.Throws<IgniteClientException>(() => 
tuple.GetOrdinal(null!));
+            ex = Assert.Throws<ArgumentException>(() => 
tuple.GetOrdinal(null!));
             Assert.AreEqual("Column name can not be null or empty.", 
ex!.Message);
 
-            ex = Assert.Throws<IgniteClientException>(() =>
+            ex = Assert.Throws<ArgumentException>(() =>
             {
                 var unused = tuple[string.Empty];
             });
             Assert.AreEqual("Column name can not be null or empty.", 
ex!.Message);
 
-            ex = Assert.Throws<IgniteClientException>(() =>
+            ex = Assert.Throws<ArgumentException>(() =>
             {
                 var unused = tuple[null!];
             });
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs
index 816b5adc6d..1ff147bb5e 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs
@@ -76,7 +76,7 @@ namespace Apache.Ignite.Tests.Table
         [Test]
         public void TestUpsertEmptyTupleThrowsException()
         {
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await TupleView.UpsertAsync(null, new IgniteTuple()));
+            var ex = Assert.ThrowsAsync<IgniteException>(async () => await 
TupleView.UpsertAsync(null, new IgniteTuple()));
 
             StringAssert.Contains(
                 "Missed key column: KEY",
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
index a11b046f4e..f3281ac82d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
@@ -69,7 +69,7 @@ namespace Apache.Ignite.Tests.Table
         {
             var pocoView = Table.GetRecordView<object>();
 
-            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => 
await pocoView.UpsertAsync(null, new object()));
+            var ex = Assert.ThrowsAsync<IgniteException>(async () => await 
pocoView.UpsertAsync(null, new object()));
 
             StringAssert.Contains("Missed key column: KEY", ex!.Message);
         }
diff --git a/modules/platforms/dotnet/Apache.Ignite.sln 
b/modules/platforms/dotnet/Apache.Ignite.sln
index 5f96ee7fce..38bd65d5d8 100644
--- a/modules/platforms/dotnet/Apache.Ignite.sln
+++ b/modules/platforms/dotnet/Apache.Ignite.sln
@@ -20,6 +20,8 @@ EndProjectSection
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
"Apache.Ignite.Benchmarks", 
"Apache.Ignite.Benchmarks\Apache.Ignite.Benchmarks.csproj", 
"{9E63F965-535F-4AF2-A380-0780EAC6390E}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
"Apache.Ignite.Internal.Generators", 
"Apache.Ignite.Internal.Generators\Apache.Ignite.Internal.Generators.csproj", 
"{7BC74F53-FF5F-4CBF-A559-9E4BE3A63A0F}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
@@ -41,5 +43,9 @@ Global
                {9E63F965-535F-4AF2-A380-0780EAC6390E}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
                {9E63F965-535F-4AF2-A380-0780EAC6390E}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
                {9E63F965-535F-4AF2-A380-0780EAC6390E}.Release|Any CPU.Build.0 
= Release|Any CPU
+               {7BC74F53-FF5F-4CBF-A559-9E4BE3A63A0F}.Debug|Any CPU.ActiveCfg 
= Debug|Any CPU
+               {7BC74F53-FF5F-4CBF-A559-9E4BE3A63A0F}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
+               {7BC74F53-FF5F-4CBF-A559-9E4BE3A63A0F}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
+               {7BC74F53-FF5F-4CBF-A559-9E4BE3A63A0F}.Release|Any CPU.Build.0 
= Release|Any CPU
        EndGlobalSection
 EndGlobal
diff --git a/modules/platforms/dotnet/Apache.Ignite/Apache.Ignite.csproj 
b/modules/platforms/dotnet/Apache.Ignite/Apache.Ignite.csproj
index 6f6466b9ea..f61653cec9 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Apache.Ignite.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite/Apache.Ignite.csproj
@@ -24,6 +24,8 @@
     <SignAssembly>true</SignAssembly>
     <AssemblyOriginatorKeyFile>Apache.Ignite.snk</AssemblyOriginatorKeyFile>
     <IsPackable>true</IsPackable>
+    <EmitCompilerGeneratedFiles>false</EmitCompilerGeneratedFiles>
+    
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
   </PropertyGroup>
 
   <ItemGroup>
@@ -39,4 +41,9 @@
     </AssemblyAttribute>
   </ItemGroup>
 
+  <ItemGroup>
+    <ProjectReference 
Include="..\Apache.Ignite.Internal.Generators\Apache.Ignite.Internal.Generators.csproj"
+                      OutputItemType="Analyzer" 
ReferenceOutputAssembly="false" 
SetTargetFramework="TargetFramework=netstandard2.0" />
+  </ItemGroup>
+
 </Project>
diff --git a/modules/platforms/dotnet/Apache.Ignite/ErrorGroups.cs 
b/modules/platforms/dotnet/Apache.Ignite/ErrorGroups.cs
new file mode 100644
index 0000000000..550101a819
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/ErrorGroups.cs
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite
+{
+    using System.Diagnostics.CodeAnalysis;
+
+    /// <summary>
+    /// Represents a concept of error group. Error group defines a collection 
of errors that belong to a single semantic component.
+    /// Each group can be identified by a name and an integer number that both 
must be unique across all error groups.
+    /// </summary>
+    [SuppressMessage(
+        "Microsoft.Design",
+        "CA1034:NestedTypesShouldNotBeVisible",
+        Justification = "Reviewed.")]
+    [SuppressMessage(
+        "Microsoft.Naming",
+        "CA1724:TypeNamesShouldNotMatchNamespaces",
+        Justification = "Types are nested, there is no conflict.")]
+    public static partial class ErrorGroups // Nested classes are generated by 
ErrorGroupGenerator source generator.
+    {
+        /// <summary>
+        /// Ignite error prefix.
+        /// </summary>
+        public const string ErrPrefix = "IGN-";
+
+        /// <summary>
+        /// Unknown error group name.
+        /// </summary>
+        public const string UnknownGroupName = "UNKNOWN";
+
+        /// <summary>
+        /// Gets error code extracted from the given full error code.
+        /// </summary>
+        /// <param name="fullCode">Full error code.</param>
+        /// <returns>Error code.</returns>
+        public static int GetErrorCode(int fullCode) => fullCode & 0xFFFF;
+
+        /// <summary>
+        /// Returns group code extracted from the given full error code.
+        /// </summary>
+        /// <param name="fullCode">Full error code.</param>
+        /// <returns>Group code.</returns>
+        public static int GetGroupCode(int fullCode) => fullCode >> 16;
+
+        /// <summary>
+        /// Gets the full error code from group and error codes.
+        /// </summary>
+        /// <param name="groupCode">Group code.</param>
+        /// <param name="errorCode">Error code.</param>
+        /// <returns>Combined code.</returns>
+        public static int GetFullCode(int groupCode, int errorCode) => 
(groupCode << 16) | (errorCode & 0xFFFF);
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConnectionException.cs 
b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConnectionException.cs
new file mode 100644
index 0000000000..eacbc76c12
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConnectionException.cs
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite
+{
+    using System;
+    using System.Diagnostics.CodeAnalysis;
+    using System.Runtime.Serialization;
+
+    /// <summary>
+    /// Ignite thin client exception.
+    /// </summary>
+    [Serializable]
+    [SuppressMessage(
+        "Microsoft.Design",
+        "CA1032:ImplementStandardExceptionConstructors",
+        Justification="Ignite exceptions use a special constructor.")]
+    public class IgniteClientConnectionException : IgniteClientException
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="IgniteClientConnectionException"/> class.
+        /// </summary>
+        /// <param name="code">Code.</param>
+        /// <param name="message">Message.</param>
+        /// <param name="innerException">Inner exception.</param>
+        public IgniteClientConnectionException(int code, string message, 
Exception? innerException = null)
+            : base(Guid.NewGuid(), code, message, innerException)
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="IgniteClientConnectionException"/> class.
+        /// </summary>
+        /// <param name="traceId">Trace id.</param>
+        /// <param name="code">Code.</param>
+        /// <param name="message">Message.</param>
+        /// <param name="innerException">Inner exception.</param>
+        public IgniteClientConnectionException(Guid traceId, int code, string 
message, Exception? innerException = null)
+            : base(traceId, code, message, innerException)
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="IgniteClientConnectionException"/> class.
+        /// </summary>
+        /// <param name="serializationInfo">Serialization information.</param>
+        /// <param name="streamingContext">Streaming context.</param>
+        protected IgniteClientConnectionException(SerializationInfo 
serializationInfo, StreamingContext streamingContext)
+            : base(serializationInfo, streamingContext)
+        {
+            // No-op.
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/IgniteClientException.cs 
b/modules/platforms/dotnet/Apache.Ignite/IgniteClientException.cs
index 5c8205afc0..3fb7fe6f32 100644
--- a/modules/platforms/dotnet/Apache.Ignite/IgniteClientException.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteClientException.cs
@@ -18,87 +18,53 @@
 namespace Apache.Ignite
 {
     using System;
+    using System.Diagnostics.CodeAnalysis;
     using System.Runtime.Serialization;
 
     /// <summary>
     /// Ignite thin client exception.
     /// </summary>
     [Serializable]
-    public class IgniteClientException : Exception
+    [SuppressMessage(
+        "Microsoft.Design",
+        "CA1032:ImplementStandardExceptionConstructors",
+        Justification="Ignite exceptions use a special constructor.")]
+    public class IgniteClientException : IgniteException
     {
-        /** Error code field. */
-        private const string ErrorCodeField = "StatusCode";
-
         /// <summary>
         /// Initializes a new instance of the <see 
cref="IgniteClientException"/> class.
         /// </summary>
-        public IgniteClientException()
-        {
-            // No-op.
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see 
cref="IgniteClientException" /> class.
-        /// </summary>
-        /// <param name="message">The message that describes the error.</param>
-        public IgniteClientException(string message)
-            : base(message)
+        /// <param name="code">Code.</param>
+        /// <param name="message">Message.</param>
+        /// <param name="innerException">Inner exception.</param>
+        public IgniteClientException(int code, string message, Exception? 
innerException = null)
+            : base(Guid.NewGuid(), code, message, innerException)
         {
             // No-op.
         }
 
         /// <summary>
-        /// Initializes a new instance of the <see 
cref="IgniteClientException" /> class.
+        /// Initializes a new instance of the <see 
cref="IgniteClientException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="cause">The cause.</param>
-        public IgniteClientException(string message, Exception? cause)
-            : base(message, cause)
+        /// <param name="traceId">Trace id.</param>
+        /// <param name="code">Code.</param>
+        /// <param name="message">Message.</param>
+        /// <param name="innerException">Inner exception.</param>
+        public IgniteClientException(Guid traceId, int code, string message, 
Exception? innerException = null)
+            : base(traceId, code, message, innerException)
         {
             // No-op.
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see 
cref="IgniteClientException" /> class.
-        /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="cause">The cause.</param>
-        /// <param name="statusCode">The error code.</param>
-        public IgniteClientException(string message, Exception? cause, int 
statusCode)
-            : base(message, cause)
-        {
-            ErrorCode = statusCode;
-        }
-
         /// <summary>
         /// Initializes a new instance of the <see 
cref="IgniteClientException"/> class.
         /// </summary>
-        /// <param name="info">Serialization information.</param>
-        /// <param name="ctx">Streaming context.</param>
-        protected IgniteClientException(SerializationInfo info, 
StreamingContext ctx)
-            : base(info, ctx)
+        /// <param name="serializationInfo">Serialization information.</param>
+        /// <param name="streamingContext">Streaming context.</param>
+        protected IgniteClientException(SerializationInfo serializationInfo, 
StreamingContext streamingContext)
+            : base(serializationInfo, streamingContext)
         {
-            ErrorCode = info.GetInt32(ErrorCodeField);
-        }
-
-        /// <summary>
-        /// Gets the error code.
-        /// </summary>
-        public int ErrorCode { get; }
-
-        /// <summary>
-        /// When overridden in a derived class, sets the <see 
cref="SerializationInfo" />
-        /// with information about the exception.
-        /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo" /> that holds 
the serialized object data
-        /// about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext" /> that 
contains contextual information
-        /// about the source or destination.</param>
-        public override void GetObjectData(SerializationInfo info, 
StreamingContext context)
-        {
-            base.GetObjectData(info, context);
-
-            info.AddValue(ErrorCodeField, ErrorCode);
+            // No-op.
         }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs 
b/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs
new file mode 100644
index 0000000000..fb8136018b
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteException.cs
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite
+{
+    using System;
+    using System.Diagnostics.CodeAnalysis;
+    using System.Runtime.Serialization;
+    using Internal.Common;
+
+    /// <summary>
+    /// Ignite exception.
+    /// </summary>
+    [Serializable]
+    [SuppressMessage(
+        "Microsoft.Design",
+        "CA1032:ImplementStandardExceptionConstructors",
+        Justification="Ignite exceptions use a special constructor.")]
+    public class IgniteException : Exception
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IgniteException"/> 
class.
+        /// </summary>
+        /// <param name="traceId">Trace id.</param>
+        /// <param name="code">Code.</param>
+        /// <param name="message">Message.</param>
+        /// <param name="innerException">Inner exception.</param>
+        public IgniteException(Guid traceId, int code, string? message, 
Exception? innerException = null)
+            : base(message, innerException)
+        {
+            TraceId = traceId;
+            Code = code;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IgniteException"/> 
class.
+        /// </summary>
+        /// <param name="serializationInfo">Serialization information.</param>
+        /// <param name="streamingContext">Streaming context.</param>
+        protected IgniteException(SerializationInfo serializationInfo, 
StreamingContext streamingContext)
+            : base(serializationInfo, streamingContext)
+        {
+            IgniteArgumentCheck.NotNull(serializationInfo, 
nameof(serializationInfo));
+
+            TraceId = (Guid)serializationInfo.GetValue(nameof(TraceId), 
typeof(Guid));
+            Code = serializationInfo.GetInt32(nameof(Code));
+        }
+
+        /// <summary>
+        /// Gets the group name.
+        /// </summary>
+        public string GroupName => 
ErrorGroups.GetGroupName(ErrorGroups.GetGroupCode(Code));
+
+        /// <summary>
+        /// Gets the full exception code.
+        /// </summary>
+        public int Code { get; }
+
+        /// <summary>
+        /// Gets the trace id (correlation id).
+        /// </summary>
+        public Guid TraceId { get; }
+
+        /// <summary>
+        /// Gets the error code.
+        /// </summary>
+        public int ErrorCode => ErrorGroups.GetErrorCode(Code);
+
+        /// <summary>
+        /// Gets the code as string.
+        /// </summary>
+        public string CodeAsString => ErrorGroups.ErrPrefix + GroupName + '-' 
+ ErrorCode;
+
+        /// <inheritdoc />
+        public override void GetObjectData(SerializationInfo info, 
StreamingContext context)
+        {
+            base.GetObjectData(info, context);
+
+            info.AddValue(nameof(Code), Code);
+            info.AddValue(nameof(TraceId), TraceId);
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
index 8f0849fe39..decaed76b8 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs
@@ -71,8 +71,8 @@ namespace Apache.Ignite.Internal
             if (configuration.Endpoints.Count == 0)
             {
                 throw new IgniteClientException(
-                    $"Invalid {nameof(IgniteClientConfiguration)}: " +
-                    $"{nameof(IgniteClientConfiguration.Endpoints)} is empty. 
Nowhere to connect.");
+                    ErrorGroups.Client.Configuration,
+                    $"Invalid {nameof(IgniteClientConfiguration)}: 
{nameof(IgniteClientConfiguration.Endpoints)} is empty. Nowhere to connect.");
             }
 
             _logger = configuration.Logger.GetLogger(GetType());
@@ -124,7 +124,7 @@ namespace Apache.Ignite.Internal
 
             if (tx.FailoverSocket != this)
             {
-                throw new IgniteClientException("Specified transaction belongs 
to a different IgniteClient instance.");
+                throw new IgniteClientException(ErrorGroups.Client.Connection, 
"Specified transaction belongs to a different IgniteClient instance.");
             }
 
             // Use tx-specific socket without retry and failover.
@@ -160,7 +160,7 @@ namespace Apache.Ignite.Internal
             {
                 if (tx.FailoverSocket != this)
                 {
-                    throw new IgniteClientException("Specified transaction 
belongs to a different IgniteClient instance.");
+                    throw new 
IgniteClientException(ErrorGroups.Client.Connection, "Specified transaction 
belongs to a different IgniteClient instance.");
                 }
 
                 // Use tx-specific socket without retry and failover.
@@ -525,7 +525,8 @@ namespace Apache.Ignite.Internal
                 errors.Add(exception);
                 var inner = new AggregateException(errors);
 
-                throw new IgniteClientException(
+                throw new IgniteClientConnectionException(
+                    ErrorGroups.Client.Connection,
                     $"Operation failed after {attempt} retries, examine 
InnerException for details.",
                     inner);
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
index 78cb555984..c4d543b9ad 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs
@@ -177,7 +177,10 @@ namespace Apache.Ignite.Internal
 
             if (ex != null)
             {
-                throw new IgniteClientException("Socket is closed due to an 
error, examine inner exception for details.", ex);
+                throw new IgniteClientConnectionException(
+                    ErrorGroups.Client.Connection,
+                    "Socket is closed due to an error, examine inner exception 
for details.",
+                    ex);
             }
 
             if (_disposeTokenSource.IsCancellationRequested)
@@ -251,8 +254,9 @@ namespace Apache.Ignite.Internal
                 {
                     if (responseMagic[i] != ProtoCommon.MagicBytes[i])
                     {
-                        throw new IgniteClientException("Invalid magic bytes 
returned from the server: " +
-                                                        
BitConverter.ToString(responseMagic));
+                        throw new IgniteClientConnectionException(
+                            ErrorGroups.Client.Protocol,
+                            "Invalid magic bytes returned from the server: " + 
BitConverter.ToString(responseMagic));
                     }
                 }
             }
@@ -268,7 +272,7 @@ namespace Apache.Ignite.Internal
 
             if (serverVer != CurrentProtocolVersion)
             {
-                throw new IgniteClientException("Unexpected server version: " 
+ serverVer);
+                throw new 
IgniteClientConnectionException(ErrorGroups.Client.Protocol, "Unexpected server 
version: " + serverVer);
             }
 
             var exception = ReadError(ref reader);
@@ -291,20 +295,19 @@ namespace Apache.Ignite.Internal
                 new ClusterNode(clusterNodeId, clusterNodeName, endPoint));
         }
 
-        private static IgniteClientException? ReadError(ref MessagePackReader 
reader)
+        private static IgniteException? ReadError(ref MessagePackReader reader)
         {
             if (reader.TryReadNil())
             {
                 return null;
             }
 
-            // TODO: IGNITE-17390 .NET: Thin 3.0: Unified exception handling - 
reconstruct correct exception.
             Guid traceId = reader.TryReadNil() ? Guid.NewGuid() : 
reader.ReadGuid();
             int code = reader.TryReadNil() ? 65537 : reader.ReadInt32();
             string className = reader.ReadString();
             string? message = reader.ReadString();
 
-            return new IgniteClientException($"{className}: {message} ({code}, 
{traceId})", null, code);
+            return ExceptionMapper.GetException(traceId, code, className, 
message);
         }
 
         private static async ValueTask<PooledBuffer> ReadResponseAsync(
@@ -358,7 +361,8 @@ namespace Apache.Ignite.Internal
                 if (res == 0)
                 {
                     // Disconnected.
-                    throw new IgniteClientException(
+                    throw new IgniteClientConnectionException(
+                        ErrorGroups.Client.Protocol,
                         "Connection lost (failed to read data from socket)",
                         new SocketException((int) 
SocketError.ConnectionAborted));
                 }
@@ -406,8 +410,8 @@ namespace Apache.Ignite.Internal
             if (configuredInterval <= TimeSpan.Zero)
             {
                 throw new IgniteClientException(
-                    
$"{nameof(IgniteClientConfiguration)}.{nameof(IgniteClientConfiguration.HeartbeatInterval)}
 " +
-                    "should be greater than zero.");
+                    ErrorGroups.Client.Configuration,
+                    
$"{nameof(IgniteClientConfiguration)}.{nameof(IgniteClientConfiguration.HeartbeatInterval)}
 should be greater than zero.");
             }
 
             if (serverIdleTimeout <= TimeSpan.Zero)
@@ -513,7 +517,7 @@ namespace Apache.Ignite.Internal
                 const string message = "Exception while reading from socket. 
Connection closed.";
 
                 _logger?.Error(message, e);
-                Dispose(new IgniteClientException(message, e));
+                Dispose(new 
IgniteClientConnectionException(ErrorGroups.Client.Connection, message, e));
             }
         }
 
@@ -535,7 +539,7 @@ namespace Apache.Ignite.Internal
             {
                 var message = $"Unexpected response ID ({requestId}) received 
from the server, closing the socket.";
                 _logger?.Error(message);
-                Dispose(new IgniteClientException(message));
+                Dispose(new 
IgniteClientConnectionException(ErrorGroups.Client.Protocol, message));
 
                 return;
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Compute.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Compute.cs
index c1db86a743..a019026ae3 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Compute.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Compute.cs
@@ -190,7 +190,7 @@ namespace Apache.Ignite.Internal.Compute
 
             _tableCache.TryRemove(tableName, out _);
 
-            throw new IgniteClientException($"Table '{tableName}' does not 
exist.");
+            throw new 
IgniteClientException(ErrorGroups.Client.TableIdNotFound, $"Table '{tableName}' 
does not exist.");
         }
 
         private async Task<T> ExecuteColocatedAsync<T, TKey>(
@@ -213,14 +213,13 @@ namespace Apache.Ignite.Internal.Compute
 
                 using var bufferWriter = Write(table, schema);
 
-                // TODO: IGNITE-17390 replace magic ErrorCode number with 
constant.
                 try
                 {
                     using var res = await 
_socket.DoOutInOpAsync(ClientOp.ComputeExecuteColocated, 
bufferWriter).ConfigureAwait(false);
 
                     return Read(res);
                 }
-                catch (IgniteClientException e) when (e.ErrorCode == 196612)
+                catch (IgniteException e) when (e.Code == 
ErrorGroups.Client.TableIdNotFound)
                 {
                     // Table was dropped - remove from cache.
                     // Try again in case a new table with the same name exists.
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Endpoint.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Endpoint.cs
index 1afd53b51b..47357434f6 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Endpoint.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Endpoint.cs
@@ -82,6 +82,7 @@ namespace Apache.Ignite.Internal
             if (string.IsNullOrWhiteSpace(endpoint))
             {
                 throw new IgniteClientException(
+                    ErrorGroups.Client.Configuration,
                     "IgniteClientConfiguration.Endpoints[...] can't be null or 
whitespace.");
             }
 
@@ -110,13 +111,16 @@ namespace Apache.Ignite.Internal
                 if (maxPort < minPort)
                 {
                     throw new IgniteClientException(
+                        ErrorGroups.Client.Configuration,
                         "Invalid format of IgniteClientConfiguration.Endpoint, 
port range is empty: " + endpoint);
                 }
 
                 return new Endpoint(host, minPort, maxPort - minPort);
             }
 
-            throw new IgniteClientException("Unrecognized format of 
IgniteClientConfiguration.Endpoint: " + endpoint);
+            throw new IgniteClientException(
+                ErrorGroups.Client.Configuration,
+                "Unrecognized format of IgniteClientConfiguration.Endpoint: " 
+ endpoint);
         }
 
         /// <summary>
@@ -132,6 +136,7 @@ namespace Apache.Ignite.Internal
             }
 
             throw new IgniteClientException(
+                ErrorGroups.Client.Configuration,
                 "Unrecognized format of IgniteClientConfiguration.Endpoint, 
failed to parse port: " + endpoint);
         }
     }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
index 781e51d58b..73a91bf5bb 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
@@ -335,7 +335,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                 case ClientDataType.Decimal:
                 default:
                     // TODO: Support all types (IGNITE-15431).
-                    throw new IgniteClientException("Unsupported type: " + 
colType);
+                    throw new 
IgniteClientException(ErrorGroups.Client.Protocol, "Unsupported type: " + 
colType);
             }
         }
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
index 7ea6189474..04adff9878 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
@@ -215,7 +215,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                 ClientDataType.String => GetString(index),
 
                 // TODO: Support all types (IGNITE-15431).
-                _ => throw new IgniteClientException("Unsupported type: " + 
columnType)
+                _ => throw new 
IgniteClientException(ErrorGroups.Client.Protocol, "Unsupported type: " + 
columnType)
             };
         }
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
index 7f51ebad72..5877fc9e0a 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
@@ -63,7 +63,7 @@ namespace Apache.Ignite.Internal.Proto
                     return reader.ReadString();
 
                 default:
-                    throw new IgniteClientException("Unsupported type: " + 
type);
+                    throw new 
IgniteClientException(ErrorGroups.Client.Protocol, "Unsupported type: " + type);
             }
         }
 
@@ -195,12 +195,14 @@ namespace Apache.Ignite.Internal.Proto
             if (hdr.TypeCode != (int)expectedType)
             {
                 throw new IgniteClientException(
+                    ErrorGroups.Client.Protocol,
                     $"Expected {expectedType} extension ({(int)expectedType}), 
but got {hdr.TypeCode}.");
             }
 
             if (expectedLength != null && hdr.Length != expectedLength)
             {
                 throw new IgniteClientException(
+                    ErrorGroups.Client.Protocol,
                     $"Expected {expectedLength} bytes for {expectedType} 
extension, but got {hdr.Length}.");
             }
         }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
index b4f7792636..71ebdce32c 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
@@ -142,7 +142,7 @@ namespace Apache.Ignite.Internal.Proto
                     return;
             }
 
-            throw new IgniteClientException("Unsupported type: " + 
obj.GetType());
+            throw new IgniteClientException(ErrorGroups.Client.Protocol, 
"Unsupported type: " + obj.GetType());
         }
 
         /// <summary>
@@ -220,7 +220,7 @@ namespace Apache.Ignite.Internal.Proto
                     return;
             }
 
-            throw new IgniteClientException("Unsupported type: " + 
obj.GetType());
+            throw new IgniteClientException(ErrorGroups.Client.Protocol, 
"Unsupported type: " + obj.GetType());
         }
 
         /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
index dddc0cead4..18caa865eb 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
@@ -19,6 +19,7 @@ namespace Apache.Ignite.Internal.Sql
 {
     using System;
     using System.Collections.Generic;
+    using System.Diagnostics;
     using System.Diagnostics.CodeAnalysis;
     using System.Threading;
     using System.Threading.Tasks;
@@ -349,10 +350,7 @@ namespace Apache.Ignite.Internal.Sql
         {
             var resourceId = _resourceId;
 
-            if (resourceId == null)
-            {
-                throw new IgniteClientException("Query has no result set.");
-            }
+            Debug.Assert(resourceId != null, "resourceId != null");
 
             if (_resourceClosed)
             {
@@ -367,12 +365,12 @@ namespace Apache.Ignite.Internal.Sql
         {
             if (!HasRowSet)
             {
-                throw new IgniteClientException("Query has no result set.");
+                throw new 
IgniteClientException(ErrorGroups.Sql.QueryNoResultSet, "Query has no result 
set.");
             }
 
             if (_iterated)
             {
-                throw new IgniteClientException("Query result set can not be 
iterated more than once.");
+                throw new IgniteClientException(ErrorGroups.Sql.CursorClosed, 
"Query result set can not be iterated more than once.");
             }
 
             _iterated = true;
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
index b461a71ab9..b107a5b6b5 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
@@ -84,7 +84,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
         public static MethodInfo GetWriteMethod(Type valueType) =>
             WriteMethods.TryGetValue(valueType, out var method)
                 ? method
-                : throw new IgniteClientException("Unsupported type: " + 
valueType);
+                : throw new 
IgniteClientException(ErrorGroups.Client.Configuration, "Unsupported type: " + 
valueType);
 
         /// <summary>
         /// Gets the read method.
@@ -94,6 +94,6 @@ namespace Apache.Ignite.Internal.Table.Serialization
         public static MethodInfo GetReadMethod(Type valueType) =>
             ReadMethods.TryGetValue(valueType, out var method)
                 ? method
-                : throw new IgniteClientException("Unsupported type: " + 
valueType);
+                : throw new 
IgniteClientException(ErrorGroups.Client.Configuration, "Unsupported type: " + 
valueType);
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
index 8fc4f257bc..b0e29bee1b 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -300,9 +300,10 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             if (fieldType != columnTypePrimary && fieldType != 
columnTypeAlternative)
             {
-                throw new IgniteClientException(
-                    $"Can't map field 
'{fieldInfo.DeclaringType?.Name}.{fieldInfo.Name}' of type '{fieldType}' " +
-                    $"to column '{column.Name}' of type '{columnTypePrimary}' 
- types do not match.");
+                var message = $"Can't map field 
'{fieldInfo.DeclaringType?.Name}.{fieldInfo.Name}' of type '{fieldType}' " +
+                              $"to column '{column.Name}' of type 
'{columnTypePrimary}' - types do not match.";
+
+                throw new 
IgniteClientException(ErrorGroups.Client.Configuration, message);
             }
         }
     }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index d40206d0a1..dfa3f90f07 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -182,7 +182,7 @@ namespace Apache.Ignite.Internal.Table
 
                 if (schemaCount == 0)
                 {
-                    throw new IgniteClientException("Schema not found: " + 
version);
+                    throw new 
IgniteClientException(ErrorGroups.Client.Protocol, "Schema not found: " + 
version);
                 }
 
                 Schema last = null!;
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs 
b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs
index b87890f7c4..0cbb00f922 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/SqlStatement.cs
@@ -70,7 +70,7 @@ namespace Apache.Ignite.Sql
             Timeout = timeout ?? DefaultTimeout;
             Schema = schema ?? DefaultSchema;
             PageSize = pageSize ?? DefaultPageSize;
-            Properties = properties == null || properties == EmptyProperties ? 
EmptyProperties : new(properties);
+            Properties = properties == null || ReferenceEquals(properties, 
EmptyProperties) ? EmptyProperties : new(properties);
         }
 
         /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs 
b/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs
index d2116985c1..b3c37ee6ff 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs
@@ -126,19 +126,19 @@ namespace Apache.Ignite.Table
             return IIgniteTuple.GetHashCode(this);
         }
 
-        private static string ParseName(string str)
+        private static string ParseName(string name)
         {
-            if (string.IsNullOrEmpty(str))
+            if (string.IsNullOrEmpty(name))
             {
-                throw new IgniteClientException("Column name can not be null 
or empty.");
+                throw new ArgumentException("Column name can not be null or 
empty.");
             }
 
-            if (str.Length > 2 && str.StartsWith('"') && str.EndsWith('"'))
+            if (name.Length > 2 && name.StartsWith('"') && name.EndsWith('"'))
             {
-                return str.Substring(1, str.Length - 2);
+                return name.Substring(1, name.Length - 2);
             }
 
-            return str.ToUpperInvariant();
+            return name.ToUpperInvariant();
         }
     }
 }
diff --git a/modules/platforms/dotnet/DEVNOTES.md 
b/modules/platforms/dotnet/DEVNOTES.md
index 80fad8db1f..745c2d1f4c 100644
--- a/modules/platforms/dotnet/DEVNOTES.md
+++ b/modules/platforms/dotnet/DEVNOTES.md
@@ -1,5 +1,5 @@
 ## Prerequisites
-* .NET Core 3.1 SDK
+* .NET 6 SDK
 * Java 11 SDK
 * Maven 3.6.0+ (for building)
 
@@ -24,6 +24,7 @@ then run .NET tests with `dotnet test` or `dotnet test 
--filter TEST_NAME`. When
 
 * Library project target `netstandard2.1`
 * Test projects target `netcoreapp3.1`
+* .NET 6 is required for source generators
 
 See [IEP-78 .NET Thin 
Client](https://cwiki.apache.org/confluence/display/IGNITE/IEP-78+.NET+Thin+Client)
 for design considerations.
 
diff --git a/modules/platforms/dotnet/Directory.Build.props 
b/modules/platforms/dotnet/Directory.Build.props
index 96f085e39b..c74a3886f2 100644
--- a/modules/platforms/dotnet/Directory.Build.props
+++ b/modules/platforms/dotnet/Directory.Build.props
@@ -18,7 +18,7 @@
 <Project>
     <PropertyGroup>
         <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-        <WarningsAsErrors/>
+        <WarningsAsErrors>CS8785</WarningsAsErrors>
         <LangVersion>9</LangVersion>
         <Nullable>enable</Nullable>
         <Version>3.0.0-alpha3</Version>

Reply via email to