ignite-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ptupit...@apache.org
Subject [ignite] branch master updated: IGNITE-12932 .NET: Add Thin Client Discovery
Date Tue, 12 May 2020 09:45:23 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/master by this push:
     new 3cadf45  IGNITE-12932 .NET: Add Thin Client Discovery
3cadf45 is described below

commit 3cadf45eef31d31a23928aee6af508406c305e75
Author: Pavel Tupitsyn <ptupitsyn@apache.org>
AuthorDate: Tue May 12 12:45:04 2020 +0300

    IGNITE-12932 .NET: Add Thin Client Discovery
    
    Add thin client discovery: client retrieves a list of server nodes with client connector endpoints and keeps this list up to date by reacting to topology change notifications in response flags.
    
    IEP: https://cwiki.apache.org/confluence/display/IGNITE/IEP-44%3A+Thin+client+cluster+discovery
---
 .../platform/client/ClientBitmaskFeature.java      |   5 +-
 .../platform/client/ClientMessageParser.java       |  17 +-
 ...ClientClusterGroupGetNodesEndpointsRequest.java |  50 +++
 ...lientClusterGroupGetNodesEndpointsResponse.java | 170 +++++++++
 .../Apache.Ignite.Core.Tests.DotNetCore.csproj     |  66 +++-
 .../Common/TestUtils.DotNetCore.cs                 |   4 +-
 .../Apache.Ignite.Core.Tests.csproj                |   7 +-
 .../Cache/Affinity/AffinityFunctionSpringTest.cs   |   5 +-
 .../Cache/Affinity/AffinityTest.cs                 |   3 +-
 .../Cache/Query/Linq/CacheLinqTest.Misc.cs         |   3 +-
 .../Client/Cache/CacheTest.cs                      |  16 +
 .../Client/Cache/CacheTestSsl.cs                   |  38 +-
 .../Client/Cache/ClientCacheConfigurationTest.cs   |   4 +-
 .../Client/Cache/PartitionAwarenessTest.cs         |  39 +-
 .../Client/ClientConnectionTest.cs                 |   6 +-
 .../Client/ClientFeaturesTest.cs                   |  86 +++++
 .../Client/ClientOpExtensionsTest.cs               |  59 ---
 .../Client/ClientProtocolCompatibilityTest.cs      |  11 +-
 .../Client/ClientReconnectCompatibilityTest.cs     |   5 +-
 .../Client/ClientTestBase.cs                       |  37 +-
 .../Client/Cluster/ClientClusterDiscoveryTests.cs  | 176 +++++++++
 .../Cluster/ClientClusterDiscoveryTestsBase.cs     | 149 ++++++++
 .../ClientClusterDiscoveryTestsBaselineTopology.cs |  74 ++++
 .../ClientClusterDiscoveryTestsNoLocalhost.cs}     |  29 +-
 .../Cluster/ClientClusterDiscoveryTestsSsl.cs}     |  29 +-
 .../Apache.Ignite.Core/Apache.Ignite.Core.csproj   |   8 +-
 .../Client/Cache/CacheClientConfiguration.cs       |   8 +-
 .../IClientConnection.cs}                          |  37 +-
 .../Apache.Ignite.Core/Client/IIgniteClient.cs     |   6 +
 .../Impl/Client/Cache/CacheClient.cs               |   8 +-
 .../Cache/ClientCacheConfigurationSerializer.cs    |  29 +-
 ...VersionAttribute.cs => ClientBitmaskFeature.cs} |  31 +-
 .../Impl/Client/ClientConnection.cs                |  71 ++++
 .../Impl/Client/ClientContextBase.cs               |  14 +-
 ...entRequestContext.cs => ClientDiscoveryNode.cs} |  56 +--
 .../Impl/Client/ClientFailoverSocket.cs            | 395 +++++++++++++++++----
 .../Impl/Client/ClientFeatures.cs                  | 227 ++++++++++++
 .../Apache.Ignite.Core/Impl/Client/ClientOp.cs     |  14 +-
 .../Impl/Client/ClientOpExtensions.cs              |  71 ----
 .../Impl/Client/ClientRequestContext.cs            |   6 +-
 .../Impl/Client/ClientResponseContext.cs           |   6 +-
 .../Apache.Ignite.Core/Impl/Client/ClientSocket.cs |  65 ++--
 .../Apache.Ignite.Core/Impl/Client/ClientUtils.cs  |  58 ---
 .../Apache.Ignite.Core/Impl/Client/Endpoint.cs     |  43 ++-
 .../Apache.Ignite.Core/Impl/Client/IgniteClient.cs |  17 +-
 modules/platforms/dotnet/DEVNOTES.txt              |   4 +-
 46 files changed, 1714 insertions(+), 548 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java
index 57e314f..fc09214 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java
@@ -31,7 +31,10 @@ public enum ClientBitmaskFeature implements ThinProtocolFeature {
     EXECUTE_TASK_BY_NAME(1),
 
     /** Cluster operations (state and WAL). */
-    CLUSTER_API(2);
+    CLUSTER_API(2),
+
+    /** Client discovery. */
+    CLUSTER_GROUP_GET_NODES_ENDPOINTS(3);
 
     /** */
     private static final EnumSet<ClientBitmaskFeature> ALL_FEATURES_AS_ENUM_SET =
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java
index 69e1e5f..37ef131 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java
@@ -67,15 +67,16 @@ import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheRe
 import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheScanQueryRequest;
 import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlFieldsQueryRequest;
 import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlQueryRequest;
-import org.apache.ignite.internal.processors.platform.client.compute.ClientExecuteTaskRequest;
-import org.apache.ignite.internal.processors.platform.client.tx.ClientTxEndRequest;
-import org.apache.ignite.internal.processors.platform.client.tx.ClientTxStartRequest;
 import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterChangeStateRequest;
 import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGetStateRequest;
-import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterWalChangeStateRequest;
-import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterWalGetStateRequest;
 import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGroupGetNodeIdsRequest;
 import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGroupGetNodesDetailsRequest;
+import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGroupGetNodesEndpointsRequest;
+import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterWalChangeStateRequest;
+import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterWalGetStateRequest;
+import org.apache.ignite.internal.processors.platform.client.compute.ClientExecuteTaskRequest;
+import org.apache.ignite.internal.processors.platform.client.tx.ClientTxEndRequest;
+import org.apache.ignite.internal.processors.platform.client.tx.ClientTxStartRequest;
 
 /**
  * Thin client message parser.
@@ -239,6 +240,9 @@ public class ClientMessageParser implements ClientListenerMessageParser {
     /** */
     private static final short OP_CLUSTER_GROUP_GET_NODE_INFO = 5101;
 
+    /** */
+    private static final short OP_CLUSTER_GROUP_GET_NODE_ENDPOINTS = 5102;
+
     /* Compute operations. */
     /** */
     private static final short OP_COMPUTE_TASK_EXECUTE = 6000;
@@ -441,6 +445,9 @@ public class ClientMessageParser implements ClientListenerMessageParser {
             case OP_CLUSTER_GROUP_GET_NODE_INFO:
                 return new ClientClusterGroupGetNodesDetailsRequest(reader);
 
+            case OP_CLUSTER_GROUP_GET_NODE_ENDPOINTS:
+                return new ClientClusterGroupGetNodesEndpointsRequest(reader);
+
             case OP_COMPUTE_TASK_EXECUTE:
                 return new ClientExecuteTaskRequest(reader);
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/cluster/ClientClusterGroupGetNodesEndpointsRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/cluster/ClientClusterGroupGetNodesEndpointsRequest.java
new file mode 100644
index 0000000..9d889da
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/cluster/ClientClusterGroupGetNodesEndpointsRequest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+package org.apache.ignite.internal.processors.platform.client.cluster;
+
+import org.apache.ignite.binary.BinaryRawReader;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+import org.apache.ignite.internal.processors.platform.client.ClientRequest;
+import org.apache.ignite.internal.processors.platform.client.ClientResponse;
+
+/**
+ * Cluster group get nodes endpoints request.
+ */
+public class ClientClusterGroupGetNodesEndpointsRequest extends ClientRequest {
+    /** Start topology version. -1 for earliest. */
+    private final long startTopVer;
+
+    /** End topology version. -1 for latest. */
+    private final long endTopVer;
+
+    /**
+     * Constructor.
+     *
+     * @param reader Reader.
+     */
+    public ClientClusterGroupGetNodesEndpointsRequest(BinaryRawReader reader) {
+        super(reader);
+        startTopVer = reader.readLong();
+        endTopVer = reader.readLong();
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientResponse process(ClientConnectionContext ctx) {
+        return new ClientClusterGroupGetNodesEndpointsResponse(requestId(), startTopVer, endTopVer);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/cluster/ClientClusterGroupGetNodesEndpointsResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/cluster/ClientClusterGroupGetNodesEndpointsResponse.java
new file mode 100644
index 0000000..204451f
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/cluster/ClientClusterGroupGetNodesEndpointsResponse.java
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+package org.apache.ignite.internal.processors.platform.client.cluster;
+
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.binary.BinaryRawWriterEx;
+import org.apache.ignite.internal.cluster.IgniteClusterEx;
+import org.apache.ignite.internal.processors.odbc.ClientListenerProcessor;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+import org.apache.ignite.internal.processors.platform.client.ClientResponse;
+
+import java.util.*;
+
+/**
+ * Cluster group get nodes endpoints response.
+ */
+public class ClientClusterGroupGetNodesEndpointsResponse extends ClientResponse {
+    /** Indicates unknown topology version. */
+    private static final long UNKNOWN_TOP_VER = -1;
+
+    /** Start topology version. -1 for earliest. */
+    private final long startTopVer;
+
+    /** End topology version. -1 for latest. */
+    private final long endTopVer;
+
+    /**
+     * Constructor.
+     *
+     * @param reqId Request identifier.
+     * @param startTopVer Start topology version.
+     * @param endTopVer End topology version.
+     */
+    public ClientClusterGroupGetNodesEndpointsResponse(long reqId,
+                                                       long startTopVer,
+                                                       long endTopVer) {
+        super(reqId);
+
+        this.startTopVer = startTopVer;
+        this.endTopVer = endTopVer;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void encode(ClientConnectionContext ctx, BinaryRawWriterEx writer) {
+        super.encode(ctx, writer);
+
+        IgniteClusterEx cluster = ctx.kernalContext().grid().cluster();
+
+        long endTopVer0 = endTopVer == UNKNOWN_TOP_VER ? cluster.topologyVersion() : endTopVer;
+
+        Collection<ClusterNode> topology = cluster.topology(endTopVer0);
+
+        writer.writeLong(endTopVer0);
+
+        if (startTopVer == UNKNOWN_TOP_VER) {
+            int pos = writer.reserveInt();
+            int size = 0;
+
+            for (ClusterNode node : topology) {
+                if (writeNode(writer, node))
+                    size++;
+            }
+
+            writer.writeInt(pos, size);
+            writer.writeInt(0);
+
+            return;
+        }
+
+        Map<UUID, ClusterNode> startNodes = toMap(cluster.topology(startTopVer));
+        Map<UUID, ClusterNode> endNodes = toMap(topology);
+
+        int pos = writer.reserveInt();
+        int cnt = 0;
+
+        for (Map.Entry<UUID, ClusterNode> endNode : endNodes.entrySet()) {
+            if (!startNodes.containsKey(endNode.getKey())) {
+                if (writeNode(writer, endNode.getValue()))
+                    cnt++;
+            }
+        }
+
+        writer.writeInt(pos, cnt);
+
+        pos = writer.reserveInt();
+        cnt = 0;
+
+        for (Map.Entry<UUID, ClusterNode> startNode : startNodes.entrySet()) {
+            if (!endNodes.containsKey(startNode.getKey()) && !startNode.getValue().isClient()) {
+                writeUuid(writer, startNode.getKey());
+                cnt++;
+            }
+        }
+
+        writer.writeInt(pos, cnt);
+    }
+
+    /**
+     * Writes node info.
+     *
+     * @param writer Writer.
+     * @param node Node.
+     */
+    private static boolean writeNode(BinaryRawWriterEx writer, ClusterNode node) {
+        if (node.isClient())
+            return false;
+
+        Object port = node.attribute(ClientListenerProcessor.CLIENT_LISTENER_PORT);
+
+        if (!(port instanceof Integer))
+            return false; // No client connector.
+
+        writeUuid(writer, node.id());
+        writer.writeInt((int) port);
+
+        Collection<String> addrs = node.addresses();
+        Collection<String> hosts = node.hostNames();
+
+        writer.writeInt(addrs.size() + hosts.size());
+
+        for (String addr : addrs)
+            writer.writeString(addr);
+
+        for (String host : hosts)
+            writer.writeString(host);
+
+        return true;
+    }
+
+    /**
+     * Writes UUID.
+     *
+     * @param writer Writer.
+     * @param id id.
+     */
+    private static void writeUuid(BinaryRawWriterEx writer, UUID id) {
+        writer.writeLong(id.getMostSignificantBits());
+        writer.writeLong(id.getLeastSignificantBits());
+    }
+
+    /**
+     * Converts collection to a set of node ids.
+     *
+     * @param nodes Nodes.
+     * @return Set of node ids.
+     */
+    private static Map<UUID, ClusterNode> toMap(Collection<ClusterNode> nodes) {
+        Map<UUID, ClusterNode> res = new HashMap<>(nodes.size());
+
+        for (ClusterNode node : nodes)
+            res.put(node.id(), node);
+
+        return res;
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj b/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj
index 5facd32..ccf7b28 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Apache.Ignite.Core.Tests.DotNetCore.csproj
@@ -92,6 +92,27 @@
       <Link>Binary\TypeNameParserTest.cs</Link>
     </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Cache\AddArgCacheEntryProcessor.cs" Link="Cache\AddArgCacheEntryProcessor.cs" />
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityAttributeTest.cs">
+      <Link>Cache\Affinity\AffinityAttributeTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityFieldTest.cs">
+      <Link>Cache\Affinity\AffinityFieldTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityFunctionSpringTest.cs">
+      <Link>Cache\Affinity\AffinityFunctionSpringTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityFunctionTest.cs">
+      <Link>Cache\Affinity\AffinityFunctionTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityKeyTest.cs">
+      <Link>Cache\Affinity\AffinityKeyTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityTest.cs">
+      <Link>Cache\Affinity\AffinityTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Cache\Affinity\AffinityTopologyVersionTest.cs">
+      <Link>Cache\Affinity\AffinityTopologyVersionTest.cs</Link>
+    </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Cache\BinarizableAddArgCacheEntryProcessor.cs" Link="Cache\BinarizableAddArgCacheEntryProcessor.cs" />
     <Compile Include="..\Apache.Ignite.Core.Tests\Cache\BinarizableTestException.cs" Link="Cache\BinarizableTestException.cs" />
     <Compile Include="..\Apache.Ignite.Core.Tests\Cache\CacheAbstractTest.cs" Link="Cache\CacheAbstractTest.cs" />
@@ -200,13 +221,13 @@
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cache\TestKeyWithAffinity.cs">
       <Link>ThinClient\Cache\TestKeyWithAffinity.cs</Link>
     </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Client\ClientFeaturesTest.cs">
+      <Link>ThinClient\ClientFeaturesTest.cs</Link>
+    </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\ClientProtocolCompatibilityTest.cs">
       <Link>ThinClient\ClientProtocolCompatibilityTest.cs</Link>
     </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\ClientConnectionTest.cs" Link="ThinClient\ClientConnectionTest.cs" />
-    <Compile Include="..\Apache.Ignite.Core.Tests\Client\ClientOpExtensionsTest.cs">
-      <Link>ThinClient\ClientOpExtensionsTest.cs</Link>
-    </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\ClientProtocolVersionTest.cs">
       <Link>ThinClient\ClientProtocolVersionTest.cs</Link>
     </Compile>
@@ -217,6 +238,21 @@
       <Link>ThinClient\ClientServerCacheAdapterExtensions.cs</Link>
     </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\ClientTestBase.cs" Link="ThinClient\ClientTestBase.cs" />
+    <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterDiscoveryTests.cs">
+      <Link>ThinClient\Cluster\ClientClusterDiscoveryTests.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterDiscoveryTestsBase.cs">
+      <Link>ThinClient\Cluster\ClientClusterDiscoveryTestsBase.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterDiscoveryTestsBaselineTopology.cs">
+      <Link>ThinClient\Cluster\ClientClusterDiscoveryTestsBaselineTopology.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterDiscoveryTestsNoLocalhost.cs">
+      <Link>ThinClient\Cluster\ClientClusterDiscoveryTestsNoLocalhost.cs</Link>
+    </Compile>
+    <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterDiscoveryTestsSsl.cs">
+      <Link>ThinClient\Cluster\ClientClusterDiscoveryTestsSsl.cs</Link>
+    </Compile>
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterGroupTests.cs" Link="ThinClient\Cluster\ClientClusterGroupTests.cs" />
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\Cluster\ClientClusterTests.cs" Link="ThinClient\Cluster\ClientClusterTests.cs" />
     <Compile Include="..\Apache.Ignite.Core.Tests\Client\EndpointTest.cs" Link="ThinClient\EndpointTest.cs" />
@@ -298,6 +334,22 @@
     <Content Include="..\Apache.Ignite.Core.Tests\Config\cache-query.xml" Link="Config\cache-query.xml">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="..\Apache.Ignite.Core.Tests\Config\Cache\Affinity\affinity-function.xml">
+      <Link>Config\Cache\Affinity\affinity-function.xml</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\Apache.Ignite.Core.Tests\Config\Cache\Affinity\affinity-function2.xml">
+      <Link>Config\Cache\Affinity\affinity-function2.xml</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\Apache.Ignite.Core.Tests\Config\Cache\Store\cache-store-session-shared-factory.xml">
+      <Link>Config\Cache\Store\cache-store-session-shared-factory.xml</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\Apache.Ignite.Core.Tests\Config\Cache\Store\cache-store-session.xml">
+      <Link>Config\Cache\Store\cache-store-session.xml</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="..\Apache.Ignite.Core.Tests\Config\Client\IgniteClientConfiguration.xml" Link="Config\Client\IgniteClientConfiguration.xml">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -316,6 +368,10 @@
     <Content Include="..\Apache.Ignite.Core.Tests\Config\Compute\compute-grid-custom-executor.xml" Link="Config\Compute\compute-grid-custom-executor.xml">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="..\Apache.Ignite.Core.Tests\Config\native-client-test-cache-affinity.xml">
+      <Link>Config\native-client-test-cache-affinity.xml</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="..\Apache.Ignite.Core.Tests\Config\native-client-test-cache-store.xml">
       <Link>Config\native-client-test-cache-store.xml</Link>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -339,9 +395,9 @@
 
   <ItemGroup>
     <PackageReference Include="log4net" Version="2.0.5" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
     <PackageReference Include="NUnit" Version="3.12.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.9.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
     <PackageReference Include="System.Configuration.ConfigurationManager" Version="4.4.0" />
 
 	<ProjectReference Include="..\Apache.Ignite.Core\Apache.Ignite.Core.DotNetCore.csproj" />
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Common/TestUtils.DotNetCore.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Common/TestUtils.DotNetCore.cs
index f524878..b38dca7 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Common/TestUtils.DotNetCore.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests.DotNetCore/Common/TestUtils.DotNetCore.cs
@@ -21,6 +21,7 @@ namespace Apache.Ignite.Core.Tests
     using System.Diagnostics;
     using System.IO;
     using System.Linq;
+    using Apache.Ignite.Core.Failure;
     using Apache.Ignite.Core.Log;
     using Apache.Ignite.Core.Tests.DotNetCore.Common;
 
@@ -43,7 +44,8 @@ namespace Apache.Ignite.Core.Tests
                 JvmOptions = TestJavaOptions(),
                 IgniteInstanceName = name,
                 Logger = TestLogger.Instance,
-                WorkDirectory = WorkDir
+                WorkDirectory = WorkDir,
+                FailureHandler = new NoOpFailureHandler()
             };
         }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
index a1aa484..17caff8 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
@@ -165,13 +165,18 @@
     <Compile Include="Client\Cache\TestKey.cs" />
     <Compile Include="Client\Cache\TestKeyWithAffinity.cs" />
     <Compile Include="Client\ClientProtocolCompatibilityTest.cs" />
-    <Compile Include="Client\ClientOpExtensionsTest.cs" />
+    <Compile Include="Client\ClientFeaturesTest.cs" />
     <Compile Include="Client\ClientProtocolVersionTest.cs" />
     <Compile Include="Client\ClientReconnectCompatibilityTest.cs" />
     <Compile Include="Client\ClientServerCacheAdapter.cs" />
     <Compile Include="Client\ClientServerCacheAdapterExtensions.cs" />
     <Compile Include="Client\ClientServerCompatibilityTest.cs" />
     <Compile Include="Client\ClientTestBase.cs" />
+    <Compile Include="Client\Cluster\ClientClusterDiscoveryTests.cs" />
+    <Compile Include="Client\Cluster\ClientClusterDiscoveryTestsBase.cs" />
+    <Compile Include="Client\Cluster\ClientClusterDiscoveryTestsBaselineTopology.cs" />
+    <Compile Include="Client\Cluster\ClientClusterDiscoveryTestsNoLocalhost.cs" />
+    <Compile Include="Client\Cluster\ClientClusterDiscoveryTestsSsl.cs" />
     <Compile Include="Client\Cluster\ClientClusterGroupTests.cs" />
     <Compile Include="Client\Cluster\ClientClusterTests.cs" />
     <Compile Include="Client\EndpointTest.cs" />
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityFunctionSpringTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityFunctionSpringTest.cs
index 14b5a60..2edb31e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityFunctionSpringTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityFunctionSpringTest.cs
@@ -21,6 +21,7 @@ namespace Apache.Ignite.Core.Tests.Cache.Affinity
 {
     using System;
     using System.Collections.Generic;
+    using System.IO;
     using System.Linq;
     using Apache.Ignite.Core.Cache;
     using Apache.Ignite.Core.Cache.Affinity;
@@ -38,8 +39,8 @@ namespace Apache.Ignite.Core.Tests.Cache.Affinity
         /// Initializes a new instance of the <see cref="AffinityFunctionSpringTest"/> class.
         /// </summary>
         public AffinityFunctionSpringTest() : base(6,
-            "config\\cache\\affinity\\affinity-function.xml",
-            "config\\cache\\affinity\\affinity-function2.xml")
+            Path.Combine("Config", "Cache", "Affinity", "affinity-function.xml"),
+            Path.Combine("Config", "Cache", "Affinity", "affinity-function2.xml"))
         {
             // No-op.
         }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs
index a3b8c03..d62c0b1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Affinity/AffinityTest.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Core.Tests.Cache.Affinity
 {
+    using System.IO;
     using Apache.Ignite.Core.Binary;
     using Apache.Ignite.Core.Cache;
     using Apache.Ignite.Core.Cluster;
@@ -37,7 +38,7 @@ namespace Apache.Ignite.Core.Tests.Cache.Affinity
             {
                 var cfg = new IgniteConfiguration(TestUtils.GetTestConfiguration())
                 {
-                    SpringConfigUrl = "config\\native-client-test-cache-affinity.xml",
+                    SpringConfigUrl = Path.Combine("Config", "native-client-test-cache-affinity.xml"),
                     IgniteInstanceName = "grid-" + i
                 };
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Misc.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Misc.cs
index 3341200..6d26131 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Misc.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Misc.cs
@@ -346,7 +346,8 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
             {
                 for (var i = 0; i < 100; i++)
                 {
-                    persons.SelectMany(p => GetRoleCache().AsCacheQueryable()).ToArray();
+                    persons.SelectMany(p => GetRoleCache().AsCacheQueryable())
+                        .Where(p => p.Value.Name.Contains("e")).ToArray();
                 }
             });
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTest.cs
index eb8c985..7ccb688 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTest.cs
@@ -34,6 +34,22 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
     public class CacheTest : ClientTestBase
     {
         /// <summary>
+        /// Initializes a new instance of the <see cref="CacheTest"/> class.
+        /// </summary>
+        public CacheTest()
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CacheTest"/> class.
+        /// </summary>
+        public CacheTest(int gridCount, bool enableSsl = false) : base(gridCount, enableSsl)
+        {
+            // No-op.
+        }
+
+        /// <summary>
         /// Tests the cache put / get with primitive data types.
         /// </summary>
         [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTestSsl.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTestSsl.cs
index 92a0737..8875117 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTestSsl.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/CacheTestSsl.cs
@@ -17,50 +17,20 @@
 
 namespace Apache.Ignite.Core.Tests.Client.Cache
 {
-    using System.IO;
-    using System.Net;
-    using System.Security.Authentication;
-    using Apache.Ignite.Core.Client;
     using NUnit.Framework;
 
     /// <summary>
-    /// Async cache test.
+    /// SSL cache test.
     /// </summary>
     [TestFixture]
     public sealed class CacheTestSsl : CacheTest
     {
         /// <summary>
-        /// Gets the Ignite configuration.
+        /// Initializes a new instance of the <see cref="CacheTestSsl"/> class.
         /// </summary>
-        protected override IgniteConfiguration GetIgniteConfiguration()
+        public CacheTestSsl() : base(1, true)
         {
-            return new IgniteConfiguration(base.GetIgniteConfiguration())
-            {
-                SpringConfigUrl = Path.Combine("Config", "Client", "server-with-ssl.xml")
-            };
-        }
-
-        /// <summary>
-        /// Gets the client configuration.
-        /// </summary>
-        protected override IgniteClientConfiguration GetClientConfiguration()
-        {
-            return new IgniteClientConfiguration(base.GetClientConfiguration())
-            {
-                Endpoints = new[] {IPAddress.Loopback + ":11110"},
-                SslStreamFactory = new SslStreamFactory
-                {
-                    CertificatePath = Path.Combine("Config", "Client", "thin-client-cert.pfx"),
-                    CertificatePassword = "123456",
-                    SkipServerCertificateValidation = true,
-                    CheckCertificateRevocation = true,
-#if !NETCOREAPP2_0 && !NETCOREAPP2_1 && !NETCOREAPP3_0
-                    SslProtocols = SslProtocols.Tls
-#else
-                    SslProtocols = SslProtocols.Tls12
-#endif
-                }
-            };
+            //No-op.
         }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/ClientCacheConfigurationTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/ClientCacheConfigurationTest.cs
index 2314d2e..6a5f079 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/ClientCacheConfigurationTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/ClientCacheConfigurationTest.cs
@@ -176,9 +176,9 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
         {
             using (var stream = new BinaryHeapStream(128))
             {
-                ClientCacheConfigurationSerializer.Write(stream, cfg, ClientSocket.CurrentProtocolVersion, true);
+                ClientCacheConfigurationSerializer.Write(stream, cfg, ClientFeatures.CurrentFeatures, true);
                 stream.Seek(0, SeekOrigin.Begin);
-                return new CacheClientConfiguration(stream, ClientSocket.CurrentProtocolVersion);
+                return new CacheClientConfiguration(stream, ClientFeatures.CurrentFeatures);
             }
         }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/PartitionAwarenessTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/PartitionAwarenessTest.cs
index 6ffb473..04cb6c5 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/PartitionAwarenessTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cache/PartitionAwarenessTest.cs
@@ -28,7 +28,6 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
     using Apache.Ignite.Core.Client;
     using Apache.Ignite.Core.Client.Cache;
     using Apache.Ignite.Core.Common;
-    using Apache.Ignite.Core.Events;
     using NUnit.Framework;
 
     /// <summary>
@@ -152,7 +151,7 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
         }
 
         [Test]
-        public void CacheGet_NewNodeEnteredTopology_RequestIsRoutedToDefaultNode()
+        public void CacheGet_NewNodeEnteredTopology_RequestIsRoutedToNewNode()
         {
             // Warm-up.
             Assert.AreEqual(1, _cache.Get(1));
@@ -168,28 +167,22 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
             var cfg = GetIgniteConfiguration();
             cfg.AutoGenerateIgniteInstanceName = true;
 
-            using (var ignite = Ignition.Start(cfg))
+            using (Ignition.Start(cfg))
             {
-                // Wait for rebalance.
-                var events = ignite.GetEvents();
-                events.EnableLocal(EventType.CacheRebalanceStopped);
-                events.WaitForLocal(EventType.CacheRebalanceStopped);
-
-                // Warm-up.
-                Assert.AreEqual(1, _cache.Get(1));
-                
-                // Get default node index by performing non-partition-aware operation.
-                _cache.GetAll(Enumerable.Range(1, 10));
-                var defaultNodeIdx = GetClientRequestGridIndex("GetAll");
-                Assert.Greater(defaultNodeIdx, -1);
-
-                // Assert: keys 12 and 14 belong to a new node now, but we don't have the new node in the server list.
-                // Requests are routed to default node.
-                Assert.AreEqual(12, _cache.Get(12));
-                Assert.AreEqual(defaultNodeIdx, GetClientRequestGridIndex());
-
-                Assert.AreEqual(14, _cache.Get(14));
-                Assert.AreEqual(defaultNodeIdx, GetClientRequestGridIndex());
+                TestUtils.WaitForTrueCondition(() =>
+                {
+                    // Keys 12 and 14 belong to a new node now (-1).
+                    Assert.AreEqual(12, _cache.Get(12));
+                    if (GetClientRequestGridIndex() != -1)
+                    {
+                        return false;
+                    }
+
+                    Assert.AreEqual(14, _cache.Get(14));
+                    Assert.AreEqual(-1, GetClientRequestGridIndex());
+
+                    return true;
+                }, 3000);
             }
         }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs
index ec90b6f..a37f2e3 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs
@@ -200,6 +200,9 @@ namespace Apache.Ignite.Core.Tests.Client
                 Assert.AreNotEqual(clientCfg, client.GetConfiguration());
                 Assert.AreNotEqual(client.GetConfiguration(), client.GetConfiguration());
                 Assert.AreEqual(clientCfg.ToXml(), client.GetConfiguration().ToXml());
+
+                var conn = client.GetConnections().Single();
+                Assert.AreEqual(servCfg.ClientConnectorConfiguration.Port, ((IPEndPoint) conn.RemoteEndPoint).Port);
             }
         }
 
@@ -340,8 +343,7 @@ namespace Apache.Ignite.Core.Tests.Client
             ignite.Dispose();
 
             var ex = Assert.Throws<AggregateException>(() => putGetTask.Wait());
-            var baseEx = ex.GetBaseException();
-            var socketEx = baseEx as SocketException;
+            var socketEx = ex.GetInnermostException() as SocketException;
 
             if (socketEx != null)
             {
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientFeaturesTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientFeaturesTest.cs
new file mode 100644
index 0000000..7b690b2
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientFeaturesTest.cs
@@ -0,0 +1,86 @@
+/*
+ * 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.Core.Tests.Client
+{
+    using System;
+    using System.Linq;
+    using Apache.Ignite.Core.Impl.Client;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="ClientFeatures"/> class.
+    /// </summary>
+    public class ClientFeaturesTest
+    {
+        /// <summary>
+        /// Tests that <see cref="ClientFeatures.GetMinVersion"/> returns a version
+        /// for every valid <see cref="ClientOp"/>.
+        /// </summary>
+        [Test]
+        public void TestGetMinVersionReturnsValueForEveryValidOp()
+        {
+            foreach (ClientOp clientOp in Enum.GetValues(typeof(ClientOp)))
+            {
+                var minVersion = ClientFeatures.GetMinVersion(clientOp);
+                
+                Assert.IsTrue(minVersion >= ClientSocket.Ver100);
+                Assert.IsTrue(minVersion <= ClientSocket.CurrentProtocolVersion);
+            }
+        }
+
+        /// <summary>
+        /// Tests that <see cref="ClientFeatures.GetMinVersion"/> returns a specific version for known new features.
+        /// </summary>
+        [Test]
+        public void TestGetMinVersionReturnsSpecificVersionForNewFeatures()
+        {
+            Assert.AreEqual(ClientSocket.Ver140, ClientFeatures.GetMinVersion(ClientOp.CachePartitions));
+
+            Assert.AreEqual(ClientSocket.Ver150, ClientFeatures.GetMinVersion(ClientOp.ClusterIsActive));
+            Assert.AreEqual(ClientSocket.Ver150, ClientFeatures.GetMinVersion(ClientOp.ClusterChangeState));
+            Assert.AreEqual(ClientSocket.Ver150, ClientFeatures.GetMinVersion(ClientOp.ClusterChangeWalState));
+            Assert.AreEqual(ClientSocket.Ver150, ClientFeatures.GetMinVersion(ClientOp.ClusterGetWalState));
+        }
+
+        /// <summary>
+        /// Tests <see cref="ClientFeatures.AllFeatures"/>.
+        /// </summary>
+        [Test]
+        public void TestAllFeatures()
+        {
+            var expected = Enum.GetValues(typeof(ClientBitmaskFeature))
+                .Cast<int>()
+                .Aggregate(0, (a, b) => a | (1 << b));
+            
+            Assert.AreEqual(expected, ClientFeatures.AllFeatures.Single());
+        }
+
+        /// <summary>
+        /// Tests <see cref="ClientFeatures.HasFeature"/>.
+        /// </summary>
+        [Test]
+        public void TestHasFeature()
+        {
+            var features = ClientFeatures.CurrentFeatures;
+            
+            Assert.IsTrue(features.HasFeature(ClientBitmaskFeature.ClusterGroupGetNodesEndpoints));
+            Assert.IsFalse(features.HasFeature((ClientBitmaskFeature) (-1)));
+            Assert.IsFalse(features.HasFeature((ClientBitmaskFeature) 12345));
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientOpExtensionsTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientOpExtensionsTest.cs
deleted file mode 100644
index 664d413..0000000
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientOpExtensionsTest.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.Core.Tests.Client
-{
-    using System;
-    using Apache.Ignite.Core.Impl.Client;
-    using NUnit.Framework;
-
-    /// <summary>
-    /// Tests for <see cref="ClientOpExtensions"/> class.
-    /// </summary>
-    public class ClientOpExtensionsTest
-    {
-        /// <summary>
-        /// Tests that <see cref="ClientOpExtensions.GetMinVersion"/> returns a version
-        /// for every valid <see cref="ClientOp"/>.
-        /// </summary>
-        [Test]
-        public void TestGetMinVersionReturnsValueForEveryValidOp()
-        {
-            foreach (ClientOp clientOp in Enum.GetValues(typeof(ClientOp)))
-            {
-                var minVersion = clientOp.GetMinVersion();
-                
-                Assert.IsTrue(minVersion >= ClientSocket.Ver100);
-                Assert.IsTrue(minVersion <= ClientSocket.CurrentProtocolVersion);
-            }
-        }
-
-        /// <summary>
-        /// Tests that <see cref="ClientOpExtensions.GetMinVersion"/> returns a specific version for known new features.
-        /// </summary>
-        [Test]
-        public void TestGetMinVersionReturnsSpecificVersionForNewFeatures()
-        {
-            Assert.AreEqual(ClientSocket.Ver140, ClientOp.CachePartitions.GetMinVersion());
-
-            Assert.AreEqual(ClientSocket.Ver150, ClientOp.ClusterIsActive.GetMinVersion());
-            Assert.AreEqual(ClientSocket.Ver150, ClientOp.ClusterChangeState.GetMinVersion());
-            Assert.AreEqual(ClientSocket.Ver150, ClientOp.ClusterChangeWalState.GetMinVersion());
-            Assert.AreEqual(ClientSocket.Ver150, ClientOp.ClusterGetWalState.GetMinVersion());
-        }
-    }
-}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientProtocolCompatibilityTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientProtocolCompatibilityTest.cs
index dad33b2..4ba7392 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientProtocolCompatibilityTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientProtocolCompatibilityTest.cs
@@ -83,10 +83,13 @@ namespace Apache.Ignite.Core.Tests.Client
                 cache.Put(1, 2);
                 Assert.AreEqual(2, cache.Get(1));
 
-                var log = GetLogs(client).Last();
-                var expectedMessage = string.Format("Partition awareness has been disabled: server protocol version " +
-                                                    "{0} is lower than required 1.4.0", version);
+                var log = GetLogs(client).FirstOrDefault(e => e.Message.StartsWith("Partition"));
+
+                var expectedMessage = string.Format(
+                    "Partition awareness has been disabled: server protocol version " +
+                    "{0} is lower than required 1.4.0", version);
                 
+                Assert.IsNotNull(log);
                 Assert.AreEqual(expectedMessage, log.Message);
                 Assert.AreEqual(LogLevel.Warn, log.Level);
                 Assert.AreEqual(typeof(ClientFailoverSocket).Name, log.Category);
@@ -131,7 +134,7 @@ namespace Apache.Ignite.Core.Tests.Client
             {
                 Assert.AreEqual(version, client.Socket.CurrentProtocolVersion);
 
-                var lastLog = GetLogs(client).Last();
+                var lastLog = GetLogs(client).Last(e => e.Level == LogLevel.Debug);
                 var expectedLog = string.Format(
                     "Handshake completed on 127.0.0.1:10800, protocol version = {0}", version);
                 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientReconnectCompatibilityTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientReconnectCompatibilityTest.cs
index d9cec91..742f59a 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientReconnectCompatibilityTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientReconnectCompatibilityTest.cs
@@ -64,7 +64,10 @@ namespace Apache.Ignite.Core.Tests.Client
                     Assert.AreEqual(42, cache.Get(1));
                     Assert.IsFalse(client.GetConfiguration().EnablePartitionAwareness);
 
-                    var log = ((ListLogger) client.GetConfiguration().Logger).Entries.Last();
+                    var log = ((ListLogger) client.GetConfiguration().Logger).Entries
+                        .FirstOrDefault(e => e.Message.StartsWith("Partition"));
+
+                    Assert.IsNotNull(log);
                     Assert.AreEqual("Partition awareness has been disabled: " +
                                     "server protocol version 1.0.0 is lower than required 1.4.0", log.Message);
                 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs
index c94eb54..c9f2b0e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientTestBase.cs
@@ -19,8 +19,10 @@ namespace Apache.Ignite.Core.Tests.Client
 {
     using System;
     using System.Collections.Generic;
+    using System.IO;
     using System.Linq;
     using System.Net;
+    using System.Security.Authentication;
     using System.Text.RegularExpressions;
     using Apache.Ignite.Core.Binary;
     using Apache.Ignite.Core.Cache;
@@ -48,6 +50,9 @@ namespace Apache.Ignite.Core.Tests.Client
         /** Grid count. */
         private readonly int _gridCount = 1;
 
+        /** SSL. */
+        private readonly bool _enableSsl;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ClientTestBase"/> class.
         /// </summary>
@@ -59,9 +64,10 @@ namespace Apache.Ignite.Core.Tests.Client
         /// <summary>
         /// Initializes a new instance of the <see cref="ClientTestBase"/> class.
         /// </summary>
-        public ClientTestBase(int gridCount)
+        public ClientTestBase(int gridCount, bool enableSsl = false)
         {
             _gridCount = gridCount;
+            _enableSsl = enableSsl;
         }
 
         /// <summary>
@@ -106,7 +112,7 @@ namespace Apache.Ignite.Core.Tests.Client
 
             Assert.AreEqual(0, cache.GetSize(CachePeekMode.All));
             Assert.AreEqual(0, GetClientCache<int>().GetSize(CachePeekMode.All));
-            
+
             ClearLoggers();
         }
 
@@ -120,7 +126,7 @@ namespace Apache.Ignite.Core.Tests.Client
         /// </summary>
         protected static ICache<int, T> GetCache<T>()
         {
-            return Ignition.GetIgnite().GetOrCreateCache<int, T>(CacheName);
+            return Ignition.GetAll().First().GetOrCreateCache<int, T>(CacheName);
         }
 
         /// <summary>
@@ -152,11 +158,27 @@ namespace Apache.Ignite.Core.Tests.Client
         /// </summary>
         protected virtual IgniteClientConfiguration GetClientConfiguration()
         {
+            var port = _enableSsl ? 11110 : IgniteClientConfiguration.DefaultPort;
+            
             return new IgniteClientConfiguration
             {
-                Endpoints = new List<string> {IPAddress.Loopback.ToString()},
+                Endpoints = new List<string> {IPAddress.Loopback + ":" + port},
                 SocketTimeout = TimeSpan.FromSeconds(15),
-                Logger = new ListLogger(new ConsoleLogger {MinLevel = LogLevel.Trace})
+                Logger = new ListLogger(new ConsoleLogger {MinLevel = LogLevel.Trace}),
+                SslStreamFactory = _enableSsl
+                    ? new SslStreamFactory
+                    {
+                        CertificatePath = Path.Combine("Config", "Client", "thin-client-cert.pfx"),
+                        CertificatePassword = "123456",
+                        SkipServerCertificateValidation = true,
+                        CheckCertificateRevocation = true,
+#if !NETCOREAPP
+                        SslProtocols = SslProtocols.Tls
+#else
+                        SslProtocols = SslProtocols.Tls12
+#endif
+                    }
+                    : null
             };
         }
 
@@ -167,14 +189,15 @@ namespace Apache.Ignite.Core.Tests.Client
         {
             return new IgniteConfiguration(TestUtils.GetTestConfiguration())
             {
-                Logger = new ListLogger()
+                Logger = new ListLogger(),
+                SpringConfigUrl = _enableSsl ? Path.Combine("Config", "Client", "server-with-ssl.xml") : null
             };
         }
 
         /// <summary>
         /// Converts object to binary form.
         /// </summary>
-        protected IBinaryObject ToBinary(object o)
+        private IBinaryObject ToBinary(object o)
         {
             return Client.GetBinary().ToBinary<IBinaryObject>(o);
         }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTests.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTests.cs
new file mode 100644
index 0000000..76662e6
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTests.cs
@@ -0,0 +1,176 @@
+/*
+ * 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.Core.Tests.Client.Cluster
+{
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Net;
+    using Apache.Ignite.Core.Tests.Client.Cache;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for client cluster discovery.
+    /// </summary>
+    public class ClientClusterDiscoveryTests : ClientClusterDiscoveryTestsBase
+    {
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientClusterDiscoveryTests"/>.
+        /// </summary>
+        public ClientClusterDiscoveryTests() : this(false, false)
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientClusterDiscoveryTests"/>.
+        /// </summary>
+        public ClientClusterDiscoveryTests(bool noLocalhost, bool enableSsl) : base(noLocalhost, enableSsl)
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Tests random topology changes.
+        /// </summary>
+        [Test]
+        public void TestClientDiscoveryWithRandomTopologyChanges()
+        {
+            var nodes = new Stack<IIgnite>();
+
+            using (var client = GetClient())
+            {
+                AssertClientConnectionCount(client, 3);
+
+                for (int i = 0; i < 20; i++)
+                {
+                    if (nodes.Count == 0 || TestUtils.Random.Next(2) == 0)
+                    {
+                        nodes.Push(Ignition.Start(GetIgniteConfiguration()));
+                    }
+                    else
+                    {
+                        nodes.Pop().Dispose();
+                    }
+                    
+                    AssertClientConnectionCount(client, 3 + nodes.Count);
+                }
+            }
+
+            foreach (var node in nodes)
+            {
+                node.Dispose();
+            }
+        }
+
+        /// <summary>
+        /// Tests that originally known node can leave and client maintains connections to other cluster nodes.
+        /// </summary>
+        [Test]
+        public void TestClientMaintainsConnectionWhenOriginalNodeLeaves()
+        {
+            // Client knows about single server node initially.
+            var ignite = Ignition.Start(GetIgniteConfiguration());
+            var cfg = GetClientConfiguration();
+            cfg.Endpoints = new[] {IPAddress.Loopback + ":10803"};
+
+            // Client starts and discovers other server nodes.
+            var client = Ignition.StartClient(cfg);
+            AssertClientConnectionCount(client, 4);
+            
+            // Original node leaves. Client is still connected.
+            ignite.Dispose();
+            AssertClientConnectionCount(client, 3);
+            
+            // Perform any operation to verify that client works.
+            Assert.AreEqual(3, client.GetCluster().GetNodes().Count);
+        }
+
+        /// <summary>
+        /// Tests that thin client discovery does not include thick client nodes.
+        /// </summary>
+        [Test]
+        public void TestThinClientDoesNotDiscoverThickClientNodes()
+        {
+            var cfg = GetIgniteConfiguration();
+            cfg.ClientMode = true;
+
+            using (Ignition.Start(cfg))
+            {
+                var client = GetClient();
+                AssertClientConnectionCount(client, 3);
+            }
+        }
+
+        /// <summary>
+        /// Tests that server nodes without client connector are ignored by thin client discovery.
+        /// </summary>
+        [Test]
+        public void TestDiscoveryWithoutClientConnectorOnServer()
+        {
+            var cfg = GetIgniteConfiguration();
+            cfg.ClientConnectorConfigurationEnabled = false;
+            
+            using (Ignition.Start(cfg))
+            {
+                var client = GetClient();
+                AssertClientConnectionCount(client, 3);
+            }
+        }
+
+        /// <summary>
+        /// Tests that Partition Awareness feature works together with Cluster Discovery.
+        /// </summary>
+        [Test]
+        public void TestPartitionAwarenessRoutesRequestsToNewlyJoinedNodes()
+        {
+            if (GetType() == typeof(ClientClusterDiscoveryTestsBaselineTopology))
+            {
+                // Fixed baseline means that rebalance to a new node won't happen.
+                return;
+            }
+            
+            var ignite = Ignition.GetAll().First();
+            var cache = ignite.CreateCache<int, int>("c");
+            
+            using (var ignite2 = Ignition.Start(GetIgniteConfiguration()))
+            {
+                var client = GetClient();
+                AssertClientConnectionCount(client, 4);
+
+                var clientCache = client.GetCache<int, int>(cache.Name);
+                var logger = (ListLogger) ignite2.Logger;
+                var aff = ignite2.GetAffinity(cache.Name);
+                var localNode = ignite2.GetCluster().GetLocalNode();
+
+                TestUtils.WaitForTrueCondition(() => aff.GetAllPartitions(localNode).Length > 0, 5000);
+                
+                var key = TestUtils.GetPrimaryKey(ignite2, cache.Name);
+                
+                TestUtils.WaitForTrueCondition(() =>
+                {
+                    clientCache.Put(key, key);
+
+                    var log = logger.Entries.LastOrDefault(
+                        e => e.Message.Contains("client.cache.ClientCachePutRequest"));
+
+                    return log != null;
+                }, 3000);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsBase.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsBase.cs
new file mode 100644
index 0000000..8c81561
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsBase.cs
@@ -0,0 +1,149 @@
+/*
+ * 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.Core.Tests.Client.Cluster
+{
+    using System;
+    using System.Linq;
+    using System.Net;
+    using Apache.Ignite.Core.Client;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Basic class for client cluster discovery tests.
+    /// </summary>
+    public abstract class ClientClusterDiscoveryTestsBase : ClientTestBase
+    {
+        /** Flag indicating whether IgniteConfiguration.Localhost should be set. */
+        private readonly bool _noLocalhost;
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientClusterDiscoveryTests"/>.
+        /// </summary>
+        public ClientClusterDiscoveryTestsBase() : this(false, false)
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientClusterDiscoveryTests"/>.
+        /// </summary>
+        public ClientClusterDiscoveryTestsBase(bool noLocalhost, bool enableSsl) : base(3, enableSsl)
+        {
+            _noLocalhost = noLocalhost;
+        }
+
+        /// <summary>
+        /// Tests that client with one initial endpoint discovers all servers.
+        /// </summary>
+        [Test]
+        public void TestClientWithOneEndpointDiscoversAllServers()
+        {
+            using (var client = GetClient())
+            {
+                AssertClientConnectionCount(client, 3);
+            }
+        }
+
+        /// <summary>
+        /// Tests that client discovers new servers automatically when they join the cluster, and removes
+        /// disconnected servers.
+        /// </summary>
+        [Test]
+        public void TestClientDiscoversJoinedServersAndRemovesDisconnected()
+        {
+            using (var client = GetClient())
+            {
+                AssertClientConnectionCount(client, 3);
+
+                using (Ignition.Start(GetIgniteConfiguration()))
+                {
+                    AssertClientConnectionCount(client, 4);
+                }
+
+                AssertClientConnectionCount(client, 3);
+            }
+        }
+        
+        /** <inheritdoc /> */
+        protected override IgniteClientConfiguration GetClientConfiguration()
+        {
+            return new IgniteClientConfiguration(base.GetClientConfiguration())
+            {
+                EnablePartitionAwareness = true
+            };
+        }
+
+        /** <inheritdoc /> */
+        protected override IgniteConfiguration GetIgniteConfiguration()
+        {
+            return new IgniteConfiguration(base.GetIgniteConfiguration())
+            {
+                Localhost = _noLocalhost ? null : "127.0.0.1",
+                AutoGenerateIgniteInstanceName = true
+            };
+        }
+        
+        /// <summary>
+        /// Asserts client connection count.
+        /// </summary>
+        protected static void AssertClientConnectionCount(IIgniteClient client, int count)
+        {
+            var res = TestUtils.WaitForCondition(() =>
+            {
+                // Perform any operation to cause topology update.
+                try
+                {
+                    client.GetCacheNames();
+                }
+                catch (Exception)
+                {
+                    // Ignore.
+                }
+
+                return count == client.GetConnections().Count();
+            }, 1000);
+
+            if (!res)
+            {
+                Assert.Fail("Client connection count mismatch: expected {0}, but was {1}", 
+                    count, client.GetConnections().Count());
+            }
+
+            var cluster = Ignition.GetAll().First().GetCluster();
+
+            foreach (var connection in client.GetConnections())
+            {
+                var server = cluster.GetNode(connection.NodeId);
+                Assert.IsNotNull(server);
+
+                var remoteEndPoint = (IPEndPoint) connection.RemoteEndPoint;
+                Assert.AreEqual(server.GetAttribute<int>("clientListenerPort"), remoteEndPoint.Port);
+
+                var ipAddresses = server.Addresses
+                    .Select(a => a.Split('%').First())  // Trim IPv6 scope.
+                    .Select(IPAddress.Parse)
+                    .ToArray();
+                
+                CollectionAssert.Contains(ipAddresses, remoteEndPoint.Address);
+
+                var localEndPoint = (IPEndPoint) connection.LocalEndPoint;
+                CollectionAssert.Contains(new[] {IPAddress.Loopback, IPAddress.IPv6Loopback}, localEndPoint.Address);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsBaselineTopology.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsBaselineTopology.cs
new file mode 100644
index 0000000..010ade2
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsBaselineTopology.cs
@@ -0,0 +1,74 @@
+/*
+ * 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.Core.Tests.Client.Cluster
+{
+    using System.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests client cluster discovery with baseline topology.
+    /// </summary>
+    [TestFixture]
+    public class ClientClusterDiscoveryTestsBaselineTopology : ClientClusterDiscoveryTests
+    {
+        /** <inheritdoc /> */
+        public override void FixtureSetUp()
+        {
+            base.FixtureSetUp();
+
+            var cluster = Ignition.GetAll().First().GetCluster();
+
+            cluster.SetBaselineAutoAdjustEnabledFlag(false);
+            cluster.SetBaselineTopology(cluster.TopologyVersion);
+        }
+
+        /// <summary>
+        /// Tests client discovery with changing baseline topology. 
+        /// </summary>
+        [Test]
+        public void TestClientDiscoveryWithBaselineTopologyChange()
+        {
+            var cache = Client.GetOrCreateCache<int, int>(TestUtils.TestName);
+            
+            AssertClientConnectionCount(Client, 3);
+            cache.Put(1, 1);
+
+            // Start new node.
+            var ignite = Ignition.Start(TestUtils.GetTestConfiguration());
+
+            AssertClientConnectionCount(Client, 4);
+            cache.Put(2, 2);
+            
+            // Add new node to baseline.
+            var cluster = ignite.GetCluster();
+            cluster.SetBaselineTopology(cluster.TopologyVersion);
+
+            AssertClientConnectionCount(Client, 4);
+            cache.Put(3, 3);
+
+            // Stop node to remove from baseline (live node can't be removed from baseline).
+            ignite.Dispose();
+            
+            cluster = Ignition.GetAll().First().GetCluster();
+            cluster.SetBaselineTopology(cluster.TopologyVersion);
+
+            AssertClientConnectionCount(Client, 3);
+            cache.Put(4, 4);
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsNoLocalhost.cs
similarity index 56%
copy from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsNoLocalhost.cs
index c218959..db07241 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsNoLocalhost.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,33 +15,22 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Core.Impl.Client
+namespace Apache.Ignite.Core.Tests.Client.Cluster
 {
-    using System;
+    using NUnit.Framework;
 
     /// <summary>
-    /// Version attribute for <see cref="ClientOp"/>.
+    /// Discovery test with no <see cref="IgniteConfiguration.Localhost"/> set.
     /// </summary>
-    [AttributeUsage(AttributeTargets.Field)]
-    internal sealed class MinVersionAttribute : Attribute
+    [TestFixture]
+    public class ClientClusterDiscoveryTestsNoLocalhost : ClientClusterDiscoveryTests
     {
-        /** */
-        private readonly ClientProtocolVersion _version;
-
-        /// <summary>
-        /// Initializes a new instance of <see cref="MinVersionAttribute"/> class.
-        /// </summary>
-        public MinVersionAttribute(short major, short minor, short maintenance)
-        {
-            _version = new ClientProtocolVersion(major, minor, maintenance);
-        }
-
         /// <summary>
-        /// Gets the version.
+        /// Initializes a new instance of <see cref="ClientClusterDiscoveryTestsNoLocalhost"/> class.
         /// </summary>
-        public ClientProtocolVersion Version
+        public ClientClusterDiscoveryTestsNoLocalhost() : base(true, false)
         {
-            get { return _version; }
+            // No-op.
         }
     }
 }
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsSsl.cs
similarity index 56%
copy from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsSsl.cs
index c218959..bf6817b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/Cluster/ClientClusterDiscoveryTestsSsl.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,33 +15,22 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Core.Impl.Client
+namespace Apache.Ignite.Core.Tests.Client.Cluster
 {
-    using System;
+    using NUnit.Framework;
 
     /// <summary>
-    /// Version attribute for <see cref="ClientOp"/>.
+    /// Tests client discovery with SSL enabled.
     /// </summary>
-    [AttributeUsage(AttributeTargets.Field)]
-    internal sealed class MinVersionAttribute : Attribute
+    [TestFixture]
+    public class ClientClusterDiscoveryTestsSsl : ClientClusterDiscoveryTestsBase
     {
-        /** */
-        private readonly ClientProtocolVersion _version;
-
-        /// <summary>
-        /// Initializes a new instance of <see cref="MinVersionAttribute"/> class.
-        /// </summary>
-        public MinVersionAttribute(short major, short minor, short maintenance)
-        {
-            _version = new ClientProtocolVersion(major, minor, maintenance);
-        }
-
         /// <summary>
-        /// Gets the version.
+        /// Initializes a new instance of <see cref="ClientClusterDiscoveryTestsSsl"/> class.
         /// </summary>
-        public ClientProtocolVersion Version
+        public ClientClusterDiscoveryTestsSsl() : base(false, true)
         {
-            get { return _version; }
+            // No-op.
         }
     }
 }
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj b/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
index 46a5965..937c9d1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
@@ -61,6 +61,7 @@
     <Compile Include="Client\IClientCluster.cs" />
     <Compile Include="Client\IClientClusterGroup.cs" />
     <Compile Include="Client\IClientClusterNode.cs" />
+    <Compile Include="Client\IClientConnection.cs" />
     <Compile Include="Client\IgniteClientConfigurationSection.cs" />
     <Compile Include="Client\ISslStreamFactory.cs" />
     <Compile Include="Client\SslStreamFactory.cs" />
@@ -93,11 +94,13 @@
     <Compile Include="Impl\Cache\QueryMetricsImpl.cs" />
     <Compile Include="Impl\Cache\Query\PlatformCacheQueryCursor.cs" />
     <Compile Include="Impl\Cache\Query\QueryCursorField.cs" />
+    <Compile Include="Impl\Client\ClientBitmaskFeature.cs" />
+    <Compile Include="Impl\Client\ClientConnection.cs" />
     <Compile Include="Impl\Client\ClientContextBase.cs" />
-    <Compile Include="Impl\Client\ClientOpExtensions.cs" />
+    <Compile Include="Impl\Client\ClientDiscoveryNode.cs" />
+    <Compile Include="Impl\Client\ClientFeatures.cs" />
     <Compile Include="Impl\Client\ClientRequestContext.cs" />
     <Compile Include="Impl\Client\ClientResponseContext.cs" />
-    <Compile Include="Impl\Client\ClientUtils.cs" />
     <Compile Include="Impl\Client\Cluster\ClientCluster.cs" />
     <Compile Include="Impl\Client\Cache\ClientCachePartitionAwarenessGroup.cs" />
     <Compile Include="Impl\Client\Cache\ClientCachePartitionMap.cs" />
@@ -108,7 +111,6 @@
     <Compile Include="Impl\Client\Cluster\ClientClusterGroupProjection.cs" />
     <Compile Include="Impl\Client\Cluster\ClientClusterNode.cs" />
     <Compile Include="Impl\Client\Endpoint.cs" />
-    <Compile Include="Impl\Client\MinVersionAttribute.cs" />
     <Compile Include="Impl\Client\SocketEndpoint.cs" />
     <Compile Include="Impl\Common\PlatformType.cs" />
     <Compile Include="Impl\Common\TaskRunner.cs" />
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/Cache/CacheClientConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/Cache/CacheClientConfiguration.cs
index d0eca45..6c9aa7d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/Cache/CacheClientConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/Cache/CacheClientConfiguration.cs
@@ -118,12 +118,12 @@ namespace Apache.Ignite.Core.Client.Cache
             {
                 using (var stream = IgniteManager.Memory.Allocate().GetStream())
                 {
-                    ClientCacheConfigurationSerializer.Write(stream, other, ClientSocket.CurrentProtocolVersion, true);
+                    ClientCacheConfigurationSerializer.Write(stream, other, ClientFeatures.CurrentFeatures, true);
 
                     stream.SynchronizeOutput();
                     stream.Seek(0, SeekOrigin.Begin);
 
-                    ClientCacheConfigurationSerializer.Read(stream, this, ClientSocket.CurrentProtocolVersion);
+                    ClientCacheConfigurationSerializer.Read(stream, this, ClientFeatures.CurrentFeatures);
                 }
 
                 CopyLocalProperties(other);
@@ -159,11 +159,11 @@ namespace Apache.Ignite.Core.Client.Cache
         /// <summary>
         /// Initializes a new instance of the <see cref="CacheClientConfiguration"/> class.
         /// </summary>
-        internal CacheClientConfiguration(IBinaryStream stream, ClientProtocolVersion srvVer)
+        internal CacheClientConfiguration(IBinaryStream stream, ClientFeatures features)
         {
             Debug.Assert(stream != null);
 
-            ClientCacheConfigurationSerializer.Read(stream, this, srvVer);
+            ClientCacheConfigurationSerializer.Read(stream, this, features);
         }
 
         /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientConnection.cs
similarity index 55%
copy from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientConnection.cs
index c218959..4924b7f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IClientConnection.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,33 +15,34 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Core.Impl.Client
+namespace Apache.Ignite.Core.Client
 {
     using System;
+    using System.Diagnostics.CodeAnalysis;
+    using System.Net;
 
     /// <summary>
-    /// Version attribute for <see cref="ClientOp"/>.
+    /// Represents Ignite client connection.
     /// </summary>
-    [AttributeUsage(AttributeTargets.Field)]
-    internal sealed class MinVersionAttribute : Attribute
+    public interface IClientConnection
     {
-        /** */
-        private readonly ClientProtocolVersion _version;
-
         /// <summary>
-        /// Initializes a new instance of <see cref="MinVersionAttribute"/> class.
+        /// Gets the remote EndPoint.
         /// </summary>
-        public MinVersionAttribute(short major, short minor, short maintenance)
-        {
-            _version = new ClientProtocolVersion(major, minor, maintenance);
-        }
+        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly",
+            Justification = "Consistency with EndPoint class name.")]
+        EndPoint RemoteEndPoint { get; }
 
         /// <summary>
-        /// Gets the version.
+        /// Gets the local EndPoint.
+        /// </summary>
+        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly",
+            Justification = "Consistency with EndPoint class name.")]
+        EndPoint LocalEndPoint { get; }
+        
+        /// <summary>
+        /// Gets the server node id.
         /// </summary>
-        public ClientProtocolVersion Version
-        {
-            get { return _version; }
-        }
+        Guid NodeId { get; }
     }
 }
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs
index f163fc6..5a94776 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs
@@ -129,5 +129,11 @@ namespace Apache.Ignite.Core.Client
         [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly",
             Justification = "Consistency with EndPoint class name.")]
         EndPoint LocalEndPoint { get; }
+
+        /// <summary>
+        /// Gets all active connections. Ignite Thin Client maintains connections to multiple server nodes when
+        /// <see cref="IgniteClientConfiguration.EnablePartitionAwareness"/> is true.
+        /// </summary>
+        IEnumerable<IClientConnection> GetConnections();
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/CacheClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/CacheClient.cs
index 4739e5f..67a3059 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/CacheClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/CacheClient.cs
@@ -560,7 +560,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         public CacheClientConfiguration GetConfiguration()
         {
             return DoOutInOp(ClientOp.CacheGetConfiguration, null,
-                ctx => new CacheClientConfiguration(ctx.Stream, ctx.ProtocolVersion));
+                ctx => new CacheClientConfiguration(ctx.Stream, ctx.Features));
         }
 
         /** <inheritDoc /> */
@@ -756,11 +756,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
 
             if (_expiryPolicy != null)
             {
-                // Check whether WithExpiryPolicy is supported by the protocol here - 
-                // ctx.ProtocolVersion refers to exact connection for this request. 
-                ClientUtils.ValidateOp(
-                    ClientCacheRequestFlag.WithExpiryPolicy, ctx.ProtocolVersion, ClientSocket.Ver150);
-                
+                ctx.Features.ValidateWithExpiryPolicyFlag();
                 ctx.Stream.WriteByte((byte) ClientCacheRequestFlag.WithExpiryPolicy);
                 ExpiryPolicySerializer.WritePolicy(ctx.Writer, _expiryPolicy);
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/ClientCacheConfigurationSerializer.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/ClientCacheConfigurationSerializer.cs
index 89443d1..95bcbee 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/ClientCacheConfigurationSerializer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Cache/ClientCacheConfigurationSerializer.cs
@@ -199,7 +199,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         /// <summary>
         /// Writes the specified config.
         /// </summary>
-        public static void Write(IBinaryStream stream, CacheClientConfiguration cfg, ClientProtocolVersion srvVer,
+        public static void Write(IBinaryStream stream, CacheClientConfiguration cfg, ClientFeatures features,
             bool skipCodes = false)
         {
             Debug.Assert(stream != null);
@@ -307,7 +307,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
             writer.WriteCollectionRaw(cfg.KeyConfiguration);
             
             code(Op.QueryEntities);
-            writer.WriteCollectionRaw(cfg.QueryEntities, (w, qe) => WriteQueryEntity(w, qe, srvVer));
+            writer.WriteCollectionRaw(cfg.QueryEntities, (w, qe) => WriteQueryEntity(w, qe, features));
 
             code(Op.ExpiryPolicy);
             ExpiryPolicySerializer.WritePolicyFactory(writer, cfg.ExpiryPolicyFactory);
@@ -320,7 +320,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         /// <summary>
         /// Write query entity of the config.
         /// </summary>
-        private static void WriteQueryEntity(BinaryWriter writer, QueryEntity entity, ClientProtocolVersion srvVer)
+        private static void WriteQueryEntity(BinaryWriter writer, QueryEntity entity, ClientFeatures features)
         {
             writer.WriteString(entity.KeyTypeName);
             writer.WriteString(entity.ValueTypeName);
@@ -336,7 +336,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
 
                 foreach (var field in entityFields)
                 {
-                    WriteQueryField(writer, field, srvVer);
+                    WriteQueryField(writer, field, features);
                 }
             }
             else
@@ -378,7 +378,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         /// <summary>
         /// Writes query field instance to the specified writer.
         /// </summary>
-        private static void WriteQueryField(BinaryWriter writer, QueryField field, ClientProtocolVersion srvVer)
+        private static void WriteQueryField(BinaryWriter writer, QueryField field, ClientFeatures features)
         {
             Debug.Assert(writer != null);
 
@@ -388,7 +388,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
             writer.WriteBoolean(field.NotNull);
             writer.WriteObject(field.DefaultValue);
 
-            if (srvVer.CompareTo(ClientSocket.Ver120) >= 0)
+            if (features.HasQueryFieldPrecisionAndScale())
             {
                 writer.WriteInt(field.Precision);
                 writer.WriteInt(field.Scale);
@@ -398,7 +398,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         /// <summary>
         /// Reads the config.
         /// </summary>
-        public static void Read(IBinaryStream stream, CacheClientConfiguration cfg, ClientProtocolVersion srvVer)
+        public static void Read(IBinaryStream stream, CacheClientConfiguration cfg, ClientFeatures features)
         {
             Debug.Assert(stream != null);
 
@@ -437,9 +437,12 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
             cfg.SqlSchema = reader.ReadString();
             cfg.WriteSynchronizationMode = (CacheWriteSynchronizationMode)reader.ReadInt();
             cfg.KeyConfiguration = reader.ReadCollectionRaw(r => new CacheKeyConfiguration(r));
-            cfg.QueryEntities = reader.ReadCollectionRaw(r => ReadQueryEntity(r, srvVer));
-            if (srvVer.CompareTo(ClientSocket.Ver160) >= 0)
+            cfg.QueryEntities = reader.ReadCollectionRaw(r => ReadQueryEntity(r, features));
+
+            if (features.HasCacheConfigurationExpiryPolicyFactory())
+            {
                 cfg.ExpiryPolicyFactory = ExpiryPolicySerializer.ReadPolicyFactory(reader);
+            }
 
             Debug.Assert(len == reader.Stream.Position - pos);
         }
@@ -447,7 +450,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         /// <summary>
         /// Reads query entity of the config.
         /// </summary>
-        private static QueryEntity ReadQueryEntity(BinaryReader reader, ClientProtocolVersion srvVer)
+        private static QueryEntity ReadQueryEntity(BinaryReader reader, ClientFeatures features)
         {
             Debug.Assert(reader != null);
 
@@ -463,7 +466,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
             var count = reader.ReadInt();
             value.Fields = count == 0
                 ? null
-                : Enumerable.Range(0, count).Select(x => ReadQueryField(reader, srvVer)).ToList();
+                : Enumerable.Range(0, count).Select(x => ReadQueryField(reader, features)).ToList();
 
             count = reader.ReadInt();
             value.Aliases = count == 0 ? null : Enumerable.Range(0, count)
@@ -478,7 +481,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
         /// <summary>
         /// Read query field.
         /// </summary>
-        private static QueryField ReadQueryField(BinaryReader reader, ClientProtocolVersion srvVer)
+        private static QueryField ReadQueryField(BinaryReader reader, ClientFeatures features)
         {
             Debug.Assert(reader != null);
 
@@ -491,7 +494,7 @@ namespace Apache.Ignite.Core.Impl.Client.Cache
                 DefaultValue = reader.ReadObject<object>()
             };
 
-            if (srvVer.CompareTo(ClientSocket.Ver120) >= 0)
+            if (features.HasQueryFieldPrecisionAndScale())
             {
                 value.Precision = reader.ReadInt();
                 value.Scale = reader.ReadInt();
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs
similarity index 54%
rename from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
rename to modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs
index c218959..cd30b88 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/MinVersionAttribute.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientBitmaskFeature.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -17,31 +17,14 @@
 
 namespace Apache.Ignite.Core.Impl.Client
 {
-    using System;
-
     /// <summary>
-    /// Version attribute for <see cref="ClientOp"/>.
+    /// Client feature ids. Values represent the index in the bit array.
     /// </summary>
-    [AttributeUsage(AttributeTargets.Field)]
-    internal sealed class MinVersionAttribute : Attribute
+    internal enum ClientBitmaskFeature
     {
-        /** */
-        private readonly ClientProtocolVersion _version;
-
-        /// <summary>
-        /// Initializes a new instance of <see cref="MinVersionAttribute"/> class.
-        /// </summary>
-        public MinVersionAttribute(short major, short minor, short maintenance)
-        {
-            _version = new ClientProtocolVersion(major, minor, maintenance);
-        }
-
-        /// <summary>
-        /// Gets the version.
-        /// </summary>
-        public ClientProtocolVersion Version
-        {
-            get { return _version; }
-        }
+        // UserAttributes = 0,
+        // ExecuteTaskByName = 1,
+        // ClusterApi = 2,
+        ClusterGroupGetNodesEndpoints = 3
     }
 }
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientConnection.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientConnection.cs
new file mode 100644
index 0000000..46c5148
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientConnection.cs
@@ -0,0 +1,71 @@
+/*
+ * 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.Core.Impl.Client
+{
+    using System;
+    using System.Diagnostics;
+    using System.Net;
+    using Apache.Ignite.Core.Client;
+
+    /// <summary>
+    /// Represents Ignite client connection.
+    /// </summary>
+    internal class ClientConnection : IClientConnection
+    {
+        /** */
+        private readonly EndPoint _localEndPoint;
+        
+        /** */
+        private readonly EndPoint _remoteEndPoint;
+
+        /** */
+        private readonly Guid _nodeId;
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientConnection"/>.
+        /// </summary>
+        public ClientConnection(EndPoint localEndPoint, EndPoint remoteEndPoint, Guid nodeId)
+        {
+            Debug.Assert(localEndPoint != null);
+            Debug.Assert(remoteEndPoint != null);
+            Debug.Assert(nodeId != Guid.Empty);
+            
+            _localEndPoint = localEndPoint;
+            _remoteEndPoint = remoteEndPoint;
+            _nodeId = nodeId;
+        }
+
+        /** <inheritdoc /> */
+        public EndPoint RemoteEndPoint
+        {
+            get { return _remoteEndPoint; }
+        }
+
+        /** <inheritdoc /> */
+        public EndPoint LocalEndPoint
+        {
+            get { return _localEndPoint; }
+        }
+
+        /** <inheritdoc /> */
+        public Guid NodeId
+        {
+            get { return _nodeId; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientContextBase.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientContextBase.cs
index 9b0b8a0..c5863f2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientContextBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientContextBase.cs
@@ -33,22 +33,22 @@ namespace Apache.Ignite.Core.Impl.Client
         private readonly Marshaller _marshaller;
 
         /** */
-        private readonly ClientProtocolVersion _protocolVersion;
+        private readonly ClientFeatures _features;
         
         /// <summary>
         /// Initializes a new instance of <see cref="ClientContextBase"/> class.
         /// </summary>
         /// <param name="stream">Stream.</param>
         /// <param name="marshaller">Marshaller.</param>
-        /// <param name="protocolVersion">Protocol version to be used for this request.</param>
-        protected ClientContextBase(IBinaryStream stream, Marshaller marshaller, ClientProtocolVersion protocolVersion)
+        /// <param name="features">Features supported by this request.</param>
+        protected ClientContextBase(IBinaryStream stream, Marshaller marshaller, ClientFeatures features)
         {
             Debug.Assert(stream != null);
             Debug.Assert(marshaller != null);
             
             _stream = stream;
             _marshaller = marshaller;
-            _protocolVersion = protocolVersion;
+            _features = features;
         }
 
         /// <summary>
@@ -68,12 +68,12 @@ namespace Apache.Ignite.Core.Impl.Client
         }
         
         /// <summary>
-        /// Protocol version to be used for this request.
+        /// Features for this request.
         /// (Takes partition awareness, failover and reconnect into account).
         /// </summary>
-        public ClientProtocolVersion ProtocolVersion
+        public ClientFeatures Features
         {
-            get { return _protocolVersion; }
+            get { return _features; }
         }
     }
 }
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientDiscoveryNode.cs
similarity index 51%
copy from modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientDiscoveryNode.cs
index d291136..e1b9b98 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientDiscoveryNode.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -17,47 +17,59 @@
 
 namespace Apache.Ignite.Core.Impl.Client
 {
-    using Apache.Ignite.Core.Impl.Binary;
-    using Apache.Ignite.Core.Impl.Binary.IO;
+    using System;
+    using System.Collections.Generic;
+    using System.Diagnostics;
 
     /// <summary>
-    /// Request context.
+    /// Represents a discovered node.
     /// </summary>
-    internal sealed class ClientRequestContext : ClientContextBase
+    internal class ClientDiscoveryNode
     {
         /** */
-        private BinaryWriter _writer;
+        private readonly Guid _id;
+
+        /** */
+        private readonly int _port;
+        
+        /** */
+        private readonly IList<string> _addresses;
 
         /// <summary>
-        /// Initializes a new instance of <see cref="ClientRequestContext"/> class.
+        /// Initializes a new instance of <see cref="ClientDiscoveryNode"/>.
         /// </summary>
-        /// <param name="stream">Stream.</param>
-        /// <param name="marshaller">Marshaller.</param>
-        /// <param name="protocolVersion">Protocol version to be used for this request.</param>
-        public ClientRequestContext(IBinaryStream stream, Marshaller marshaller, ClientProtocolVersion protocolVersion)
-            : base(stream, marshaller, protocolVersion)
+        public ClientDiscoveryNode(Guid id, int port, IList<string> addresses)
+        {
+            Debug.Assert(addresses != null);
+            Debug.Assert(addresses.Count > 0);
+            
+            _id = id;
+            _port = port;
+            _addresses = addresses;
+        }
 
+        /// <summary>
+        /// Gets the id.
+        /// </summary>
+        public Guid Id
         {
-            // No-op.
+            get { return _id; }
         }
 
         /// <summary>
-        /// Writer.
+        /// Gets the port.
         /// </summary>
-        public BinaryWriter Writer
+        public int Port
         {
-            get { return _writer ?? (_writer = Marshaller.StartMarshal(Stream)); }
+            get { return _port; }
         }
 
         /// <summary>
-        /// Finishes marshal session for this request (if any).
+        /// Gets the addresses - IPs or host names.
         /// </summary>
-        public void FinishMarshal()
+        public IList<string> Addresses
         {
-            if (_writer != null)
-            {
-                Marshaller.FinishMarshal(_writer);
-            }
+            get { return _addresses; }
         }
     }
 }
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs
index d3e97ee..5e35711 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFailoverSocket.cs
@@ -39,6 +39,9 @@ namespace Apache.Ignite.Core.Impl.Client
     /// </summary>
     internal class ClientFailoverSocket : IDisposable
     {
+        /** Unknown topology version. */
+        private const long UnknownTopologyVersion = -1;
+        
         /** Underlying socket. */
         private ClientSocket _socket;
 
@@ -51,24 +54,36 @@ namespace Apache.Ignite.Core.Impl.Client
         /** Marshaller. */
         private readonly Marshaller _marsh;
 
-        /** Endpoints with corresponding hosts. */
+        /** Endpoints with corresponding hosts - from config. */
         private readonly List<SocketEndpoint> _endPoints;
 
-        /** Locker. */
-        private readonly object _syncRoot = new object();
+        /** Map from node ID to connected socket. */
+        private volatile Dictionary<Guid, ClientSocket> _nodeSocketMap = new Dictionary<Guid, ClientSocket>();
+
+        /** Discovered nodes map. Represents current topology. */
+        private volatile Dictionary<Guid, ClientDiscoveryNode> _discoveryNodes;
+
+        /** Main socket lock. */
+        private readonly object _socketLock = new object();
+
+        /** Topology change lock. */
+        private readonly object _topologyUpdateLock = new object();
 
         /** Disposed flag. */
-        private bool _disposed;
+        private volatile bool _disposed;
 
         /** Current affinity topology version. Store as object to make volatile. */
         private volatile object _affinityTopologyVersion;
 
-        /** Map from node ID to connected socket. */
-        private volatile Dictionary<Guid, ClientSocket> _nodeSocketMap;
+        /** Topology version that <see cref="_discoveryNodes"/> corresponds to. */
+        private long _discoveryTopologyVersion = UnknownTopologyVersion;
 
         /** Map from cache ID to partition mapping. */
         private volatile ClientCacheTopologyPartitionMap _distributionMap;
 
+        /** Enable discovery flag. */
+        private volatile bool _enableDiscovery = true;
+
         /** Distribution map locker. */
         private readonly object _distributionMapSyncRoot = new object();
 
@@ -106,7 +121,7 @@ namespace Apache.Ignite.Core.Impl.Client
             _logger = (_config.Logger ?? NoopLogger.Instance).GetLogger(GetType());
             
             Connect();
-       }
+        }
 
         /// <summary>
         /// Performs a send-receive operation.
@@ -175,7 +190,7 @@ namespace Apache.Ignite.Core.Impl.Client
         {
             get
             {
-                var socket = _socket;
+                var socket = GetSocket();
                 return socket != null ? socket.RemoteEndPoint : null;
             }
         }
@@ -187,17 +202,52 @@ namespace Apache.Ignite.Core.Impl.Client
         {
             get
             {
-                var socket = _socket;
+                var socket = GetSocket();
                 return socket != null ? socket.LocalEndPoint : null;
             }
         }
 
         /// <summary>
+        /// Gets active connections.
+        /// </summary>
+        public IEnumerable<IClientConnection> GetConnections()
+        {
+            var map = _nodeSocketMap;
+
+            foreach (var socket in map.Values)
+            {
+                if (!socket.IsDisposed)
+                {
+                    yield return new ClientConnection(socket.LocalEndPoint, socket.RemoteEndPoint,
+                        socket.ServerNodeId.GetValueOrDefault());
+                }
+            }
+
+            foreach (var socketEndpoint in _endPoints)
+            {
+                var socket = socketEndpoint.Socket;
+
+                if (socket == null || socket.IsDisposed)
+                {
+                    continue;
+                }
+
+                if (socket.ServerNodeId != null && map.ContainsKey(socket.ServerNodeId.Value))
+                {
+                    continue;
+                }
+                
+                yield return new ClientConnection(socket.LocalEndPoint, socket.RemoteEndPoint,
+                    socket.ServerNodeId.GetValueOrDefault());
+            }
+        }
+
+        /// <summary>
         /// Checks the disposed state.
         /// </summary>
         private ClientSocket GetSocket()
         {
-            lock (_syncRoot)
+            lock (_socketLock)
             {
                 ThrowIfDisposed();
 
@@ -212,6 +262,8 @@ namespace Apache.Ignite.Core.Impl.Client
 
         private ClientSocket GetAffinitySocket<TKey>(int cacheId, TKey key)
         {
+            ThrowIfDisposed();
+            
             if (!_config.EnablePartitionAwareness)
             {
                 return null;
@@ -262,7 +314,8 @@ namespace Apache.Ignite.Core.Impl.Client
             Justification = "There is no finalizer.")]
         public void Dispose()
         {
-            lock (_syncRoot)
+            lock (_socketLock)
+            lock (_topologyUpdateLock)
             {
                 _disposed = true;
 
@@ -281,18 +334,36 @@ namespace Apache.Ignite.Core.Impl.Client
 
                     _nodeSocketMap = null;
                 }
+
+                foreach (var socketEndpoint in _endPoints)
+                {
+                    if (socketEndpoint.Socket != null)
+                    {
+                        socketEndpoint.Socket.Dispose();
+                    }
+                }
             }
         }
 
         /// <summary>
-        /// Connects the socket.
+        /// Gets next connected socket, or connects a new one.
         /// </summary>
-        private void Connect()
+        private ClientSocket GetNextSocket()
         {
             List<Exception> errors = null;
             var startIdx = (int) Interlocked.Increment(ref _endPointIndex);
-            _socket = null;
 
+            // Check socket map first, if available: it includes all cluster nodes.
+            var map = _nodeSocketMap;
+            foreach (var socket in map.Values)
+            {
+                if (!socket.IsDisposed)
+                {
+                    return socket;
+                }
+            }
+
+            // Fall back to initially known endpoints.
             for (var i = 0; i < _endPoints.Count; i++)
             {
                 var idx = (startIdx + i) % _endPoints.Count;
@@ -300,18 +371,12 @@ namespace Apache.Ignite.Core.Impl.Client
 
                 if (endPoint.Socket != null && !endPoint.Socket.IsDisposed)
                 {
-                    _socket = endPoint.Socket;
-                    break;
+                    return endPoint.Socket;
                 }
 
                 try
                 {
-                    _socket = new ClientSocket(_config, endPoint.EndPoint, endPoint.Host, 
-                        _config.ProtocolVersion, OnAffinityTopologyVersionChange, _marsh);
-
-                    endPoint.Socket = _socket;
-
-                    break;
+                    return Connect(endPoint);
                 }
                 catch (SocketException e)
                 {
@@ -324,69 +389,125 @@ namespace Apache.Ignite.Core.Impl.Client
                 }
             }
 
-            if (_socket == null && errors != null)
-            {
-                throw new AggregateException("Failed to establish Ignite thin client connection, " +
-                                             "examine inner exceptions for details.", errors);
-            }
+            throw new AggregateException("Failed to establish Ignite thin client connection, " +
+                                         "examine inner exceptions for details.", errors);
+        }
 
-            if (_socket != null &&
-                _config.EnablePartitionAwareness &&
-                _socket.ServerVersion < ClientOp.CachePartitions.GetMinVersion())
+        /// <summary>
+        /// Connects the socket.
+        /// </summary>
+        private void Connect()
+        {
+            _socket = GetNextSocket();
+
+            if (_config.EnablePartitionAwareness && !_socket.Features.HasOp(ClientOp.CachePartitions))
             {
                 _config.EnablePartitionAwareness = false;
 
                 _logger.Warn("Partition awareness has been disabled: server protocol version {0} " +
                              "is lower than required {1}",
                     _socket.ServerVersion,
-                    ClientOp.CachePartitions.GetMinVersion()
+                    ClientFeatures.GetMinVersion(ClientOp.CachePartitions)
                 );
             }
+
+            if (!_socket.Features.HasFeature(ClientBitmaskFeature.ClusterGroupGetNodesEndpoints))
+            {
+                _enableDiscovery = false;
+
+                _logger.Warn("Automatic server node discovery is not supported by the server");
+            }
+        }
+
+        /// <summary>
+        /// Connects to the given endpoint.
+        /// </summary>
+        private ClientSocket Connect(SocketEndpoint endPoint)
+        {
+            var socket = new ClientSocket(_config, endPoint.EndPoint, endPoint.Host,
+                _config.ProtocolVersion, OnAffinityTopologyVersionChange, _marsh);
+
+            endPoint.Socket = socket;
+
+            return socket;
         }
 
         /// <summary>
         /// Updates current Affinity Topology Version.
         /// </summary>
+        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+            Justification = "Thread root must catch all exceptions to avoid crashing the process.")]
         private void OnAffinityTopologyVersionChange(AffinityTopologyVersion affinityTopologyVersion)
         {
             _affinityTopologyVersion = affinityTopologyVersion;
 
-            if (_config.EnablePartitionAwareness)
+            if (_discoveryTopologyVersion < affinityTopologyVersion.Version &&_config.EnablePartitionAwareness)
             {
-                InitSocketMap();
+                ThreadPool.QueueUserWorkItem(_ =>
+                {
+                    try
+                    {
+                        lock (_topologyUpdateLock)
+                        {
+                            if (!_disposed)
+                            {
+                                DiscoverEndpoints();
+                                InitSocketMap();
+                            }
+                        }
+                    }
+                    catch (Exception e)
+                    {
+                        _logger.Log(LogLevel.Error, e, "Failed to update topology information");
+                    }
+                });
             }
         }
 
         /// <summary>
         /// Gets the endpoints: all combinations of IP addresses and ports according to configuration.
         /// </summary>
-        private static IEnumerable<SocketEndpoint> GetIpEndPoints(IgniteClientConfiguration cfg)
+        private IEnumerable<SocketEndpoint> GetIpEndPoints(IgniteClientConfiguration cfg)
         {
             foreach (var e in Endpoint.GetEndpoints(cfg))
             {
                 var host = e.Host;
                 Debug.Assert(host != null);  // Checked by GetEndpoints.
 
-                // GetHostEntry accepts IPs, but TryParse is a more efficient shortcut.
-                IPAddress ip;
-
-                if (IPAddress.TryParse(host, out ip))
+                for (var port = e.Port; port <= e.PortRange + e.Port; port++)
                 {
-                    for (var i = 0; i <= e.PortRange; i++)
+                    foreach (var ip in GetIps(e.Host))
                     {
-                        yield return new SocketEndpoint(new IPEndPoint(ip, e.Port + i), host);
+                        yield return new SocketEndpoint(new IPEndPoint(ip, port), e.Host);
                     }
                 }
-                else
+            }
+        }
+
+        /// <summary>
+        /// Gets IP address list from a given host.
+        /// When host is an IP already - parses it. Otherwise, resolves DNS name to IPs.
+        /// </summary>
+        private IEnumerable<IPAddress> GetIps(string host, bool suppressExceptions = false)
+        {
+            try
+            {
+                IPAddress ip;
+
+                // GetHostEntry accepts IPs, but TryParse is a more efficient shortcut.
+                return IPAddress.TryParse(host, out ip) ? new[] {ip} : Dns.GetHostEntry(host).AddressList;
+
+            }
+            catch (SocketException e)
+            {
+                _logger.Debug(e, "Failed to parse host: " + host);
+
+                if (suppressExceptions)
                 {
-                    for (var i = 0; i <= e.PortRange; i++)
-                    {
-                        foreach (var x in Dns.GetHostEntry(host).AddressList)
-                        {
-                            yield return new SocketEndpoint(new IPEndPoint(x, e.Port + i), host);
-                        }
-                    }
+                    return Enumerable.Empty<IPAddress>();
                 }
+
+                throw;
             }
         }
 
@@ -521,33 +642,187 @@ namespace Apache.Ignite.Core.Impl.Client
 
         private void InitSocketMap()
         {
-            var map = new Dictionary<Guid, ClientSocket>();
+            var map = new Dictionary<Guid, ClientSocket>(_nodeSocketMap);
 
-            foreach (var endPoint in _endPoints)
+            var defaultSocket = _socket;
+            if (defaultSocket != null && !defaultSocket.IsDisposed && defaultSocket.ServerNodeId != null)
             {
-                if (endPoint.Socket == null || endPoint.Socket.IsDisposed)
+                map[defaultSocket.ServerNodeId.Value] = defaultSocket;
+            }
+
+            if (_discoveryNodes != null)
+            {
+                // Discovery enabled: make sure we have connection to all nodes in the cluster.
+                foreach (var node in _discoveryNodes.Values)
                 {
-                    try
+                    ClientSocket socket;
+                    if (map.TryGetValue(node.Id, out socket) && !socket.IsDisposed)
                     {
-                        var socket = new ClientSocket(_config, endPoint.EndPoint, endPoint.Host, 
-                            _config.ProtocolVersion, OnAffinityTopologyVersionChange, _marsh);
+                        continue;
+                    }
+
+                    socket = TryConnect(node);
 
-                        endPoint.Socket = socket;
+                    if (socket != null)
+                    {
+                        map[node.Id] = socket;
                     }
-                    catch (SocketException)
+                }
+
+                // Dispose and remove any connections not in current topology.
+                var toRemove = new List<Guid>();
+                
+                foreach (var pair in map)
+                {
+                    if (!_discoveryNodes.ContainsKey(pair.Key))
                     {
-                        continue;
+                        pair.Value.Dispose();
+                        toRemove.Add(pair.Key);
                     }
                 }
 
-                var nodeId = endPoint.Socket.ServerNodeId;
-                if (nodeId != null)
+                foreach (var nodeId in toRemove)
                 {
-                    map[nodeId.Value] = endPoint.Socket;
+                    map.Remove(nodeId);
                 }
             }
+            else
+            {
+                // Discovery disabled: fall back to endpoints from config.
+                foreach (var endPoint in _endPoints)
+                {
+                    if (endPoint.Socket == null || endPoint.Socket.IsDisposed)
+                    {
+                        try
+                        {
+                            Connect(endPoint);
+                        }
+                        catch (SocketException)
+                        {
+                            continue;
+                        }
+                    }
 
+                    // ReSharper disable once PossibleNullReferenceException (Connect ensures that).
+                    var nodeId = endPoint.Socket.ServerNodeId;
+                    if (nodeId != null)
+                    {
+                        map[nodeId.Value] = endPoint.Socket;
+                    }
+                }
+            }
+            
             _nodeSocketMap = map;
         }
+
+        private ClientSocket TryConnect(ClientDiscoveryNode node)
+        {
+            foreach (var addr in node.Addresses)
+            {
+                foreach (var ip in GetIps(addr, true))
+                {
+                    try
+                    {
+                        var ipEndpoint = new IPEndPoint(ip, node.Port);
+
+                        var socket = new ClientSocket(_config, ipEndpoint, addr,
+                            _config.ProtocolVersion, OnAffinityTopologyVersionChange, _marsh);
+
+                        if (socket.ServerNodeId == node.Id)
+                        {
+                            return socket;
+                        }
+
+                        _logger.Debug(
+                            "Autodiscovery connection succeeded, but node id does not match: {0}, {1}. " +
+                            "Expected node id: {2}. Actual node id: {3}. Connection dropped.",
+                            addr, node.Port, node.Id, socket.ServerNodeId);
+                    }
+                    catch (SocketException socketEx)
+                    {
+                        // Ignore: failure to connect is expected.
+                        _logger.Debug(socketEx, "Autodiscovery connection failed: {0}, {1}", addr, node.Port);
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Updates endpoint info.
+        /// </summary>
+        private void DiscoverEndpoints()
+        {
+            if (!_enableDiscovery)
+            {
+                return;
+            }
+            
+            var newVer = GetTopologyVersion();
+
+            if (newVer <= _discoveryTopologyVersion)
+            {
+                return;
+            }
+
+            var discoveryNodes = _discoveryNodes == null
+                ? new Dictionary<Guid, ClientDiscoveryNode>()
+                : new Dictionary<Guid, ClientDiscoveryNode>(_discoveryNodes);
+
+            _discoveryTopologyVersion = GetServerEndpoints(
+                _discoveryTopologyVersion, newVer, discoveryNodes);
+            
+            _discoveryNodes = discoveryNodes;
+        }
+
+        /// <summary>
+        /// Gets all server endpoints.
+        /// </summary>
+        private long GetServerEndpoints(long startTopVer, long endTopVer, IDictionary<Guid, ClientDiscoveryNode> dict)
+        {
+            return DoOutInOp(ClientOp.ClusterGroupGetNodesEndpoints,
+                ctx =>
+                {
+                    ctx.Writer.WriteLong(startTopVer);
+                    ctx.Writer.WriteLong(endTopVer);
+                },
+                ctx =>
+                {
+                    var s = ctx.Stream;
+
+                    var topVer = s.ReadLong();
+
+                    var addedCnt = s.ReadInt();
+
+                    for (var i = 0; i < addedCnt; i++)
+                    {
+                        var id = BinaryUtils.ReadGuid(s);
+                        var port = s.ReadInt();
+                        var addresses = ctx.Reader.ReadStringCollection();
+                        
+                        dict[id] = new ClientDiscoveryNode(id, port, addresses);
+                    }
+
+                    var removedCnt = s.ReadInt();
+
+                    for (var i = 0; i < removedCnt; i++)
+                    {
+                        dict.Remove(BinaryUtils.ReadGuid(s));
+                    }
+                    
+                    return topVer;
+                });
+        }
+
+        /// <summary>
+        /// Gets current topology version.
+        /// </summary>
+        private long GetTopologyVersion()
+        {
+            var ver = _affinityTopologyVersion;
+            
+            return ver == null ? UnknownTopologyVersion : ((AffinityTopologyVersion) ver).Version;
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFeatures.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFeatures.cs
new file mode 100644
index 0000000..3e782f1
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientFeatures.cs
@@ -0,0 +1,227 @@
+/*
+ * 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.Core.Impl.Client
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using System.Linq;
+    using Apache.Ignite.Core.Cache.Configuration;
+    using Apache.Ignite.Core.Client;
+
+    /// <summary>
+    /// Handles client features based on protocol version and feature flags.
+    /// </summary>
+    internal class ClientFeatures
+    {
+        /** Bit mask of all features. */
+        public static readonly byte[] AllFeatures = GetAllFeatures();
+
+        /** Current features. */
+        public static readonly ClientFeatures CurrentFeatures =
+            new ClientFeatures(ClientSocket.CurrentProtocolVersion, new BitArray(AllFeatures));
+
+        /** */
+        private static readonly Dictionary<ClientOp, ClientProtocolVersion> OpVersion =
+            new Dictionary<ClientOp, ClientProtocolVersion>
+            {
+                {ClientOp.CachePartitions, new ClientProtocolVersion(1, 4, 0)},
+                {ClientOp.ClusterIsActive, new ClientProtocolVersion(1, 5, 0)},
+                {ClientOp.ClusterChangeState, new ClientProtocolVersion(1, 5, 0)},
+                {ClientOp.ClusterChangeWalState, new ClientProtocolVersion(1, 5, 0)},
+                {ClientOp.ClusterGetWalState, new ClientProtocolVersion(1, 5, 0)},
+                {ClientOp.ClusterGroupGetNodeIds, new ClientProtocolVersion(1, 5, 0)},
+                {ClientOp.ClusterGroupGetNodesInfo, new ClientProtocolVersion(1, 5, 0)},
+            };
+        
+        /** */
+        private static readonly Dictionary<ClientOp, ClientBitmaskFeature> OpFeature = 
+            new Dictionary<ClientOp, ClientBitmaskFeature>
+        {
+            {ClientOp.ClusterGroupGetNodesEndpoints, ClientBitmaskFeature.ClusterGroupGetNodesEndpoints}
+        };
+        
+        /** */
+        private readonly ClientProtocolVersion _protocolVersion;
+
+        /** */
+        private readonly BitArray _features;
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ClientFeatures"/>. 
+        /// </summary>
+        public ClientFeatures(ClientProtocolVersion protocolVersion, BitArray features)
+        {
+            _protocolVersion = protocolVersion;
+            _features = features;
+        }
+
+        /// <summary>
+        /// Returns a value indicating whether specified feature is supported.
+        /// </summary>
+        public bool HasFeature(ClientBitmaskFeature feature)
+        {
+            var index = (int) feature;
+
+            return _features != null && index >= 0 && index < _features.Count && _features.Get(index);
+        }
+
+        /// <summary>
+        /// Returns a value indicating whether specified operation is supported.
+        /// </summary>
+        public bool HasOp(ClientOp op)
+        {
+            return ValidateOp(op, false);
+        }
+
+        /// <summary>
+        /// Checks whether WithExpiryPolicy request flag is supported. Throws an exception when not supported. 
+        /// </summary>
+        public void ValidateWithExpiryPolicyFlag()
+        {
+            var requiredVersion = ClientSocket.Ver150;
+
+            if (_protocolVersion < requiredVersion)
+            {
+                ThrowMinimumVersionException("WithExpiryPolicy", requiredVersion);
+            }
+        }
+
+        /// <summary>
+        /// Returns a value indicating whether <see cref="QueryField.Precision"/> and <see cref="QueryField.Scale"/>
+        /// are supported.
+        /// </summary>
+        public bool HasQueryFieldPrecisionAndScale()
+        {
+            return _protocolVersion >= ClientSocket.Ver120;
+        }
+        
+        /// <summary>
+        /// Returns a value indicating whether <see cref="CacheConfiguration.ExpiryPolicyFactory"/> is supported.
+        /// </summary>
+        public bool HasCacheConfigurationExpiryPolicyFactory()
+        {
+            return _protocolVersion >= ClientSocket.Ver160;
+        }
+        
+        /// <summary>
+        /// Gets minimum protocol version that is required to perform specified operation.
+        /// </summary>
+        /// <param name="op">Operation.</param>
+        /// <returns>Minimum protocol version.</returns>
+        public static ClientProtocolVersion GetMinVersion(ClientOp op)
+        {
+            ClientProtocolVersion minVersion;
+            
+            return OpVersion.TryGetValue(op, out minVersion) 
+                ? minVersion 
+                : ClientSocket.Ver100;
+        }
+
+        /// <summary>
+        /// Validates specified op code against current protocol version and features.
+        /// </summary>
+        /// <param name="operation">Operation.</param>
+        public void ValidateOp(ClientOp operation)
+        {
+            ValidateOp(operation, true);
+        }
+        
+        /// <summary>
+        /// Validates specified op code against current protocol version and features.
+        /// </summary>
+        private bool ValidateOp(ClientOp operation, bool shouldThrow)
+        {
+            var requiredProtocolVersion = GetMinVersion(operation);
+            
+            if (_protocolVersion < requiredProtocolVersion)
+            {
+                if (shouldThrow)
+                {
+                    ThrowMinimumVersionException(operation, requiredProtocolVersion);
+                }
+
+                return false;
+            }
+
+            var requiredFeature = GetFeature(operation);
+
+            if (_features != null && requiredFeature != null && !_features.Get((int) requiredFeature.Value))
+            {
+                if (shouldThrow)
+                {
+                    throw new IgniteClientException(string.Format(
+                        "Operation {0} is not supported by the server. Feature {1} is missing.",
+                        operation, requiredFeature.Value));
+                }
+
+                return false;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Throws minimum version exception.
+        /// </summary>
+        private void ThrowMinimumVersionException(object operation, ClientProtocolVersion requiredProtocolVersion)
+        {
+            var message = string.Format("Operation {0} is not supported by protocol version {1}. " +
+                                        "Minimum protocol version required is {2}.",
+                operation, _protocolVersion, requiredProtocolVersion);
+
+            throw new IgniteClientException(message);
+        }
+
+        /// <summary>
+        /// Gets <see cref="ClientBitmaskFeature"/> that is required to perform specified operation.
+        /// </summary>
+        /// <param name="op">Operation.</param>
+        /// <returns>Required feature flag, or null.</returns>
+        private static ClientBitmaskFeature? GetFeature(ClientOp op)
+        {
+            ClientBitmaskFeature feature;
+
+            return OpFeature.TryGetValue(op, out feature)
+                ? feature
+                : (ClientBitmaskFeature?) null;
+        }
+
+        /// <summary>
+        /// Gets a bit array with all supported features.
+        /// </summary>
+        private static byte[] GetAllFeatures()
+        {
+            var values = Enum.GetValues(typeof(ClientBitmaskFeature))
+                .Cast<int>()
+                .ToArray();
+
+            var bits = new BitArray(values.Max() + 1);
+
+            foreach (var feature in values)
+            {
+                bits.Set(feature, true);
+            }
+            
+            var bytes = new byte[1 + values.Length / 8];
+            bits.CopyTo(bytes, 0);
+
+            return bytes;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
index 91ce442..e75fbc9 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
@@ -54,8 +54,6 @@ namespace Apache.Ignite.Core.Impl.Client
         CacheGetOrCreateWithConfiguration = 1054,
         CacheGetConfiguration = 1055,
         CacheDestroy = 1056,
-        
-        [MinVersion(1, 4, 0)]
         CachePartitions = 1101,
         
         // Queries.
@@ -73,20 +71,12 @@ namespace Apache.Ignite.Core.Impl.Client
         BinaryTypePut = 3003,
 
         // Cluster.
-        [MinVersion(1, 5, 0)]
         ClusterIsActive = 5000,
-        
-        [MinVersion(1, 5, 0)]
         ClusterChangeState = 5001,
-        
-        [MinVersion(1, 5, 0)]
         ClusterChangeWalState = 5002,
-        
-        [MinVersion(1, 5, 0)]
         ClusterGetWalState = 5003,
-        [MinVersion(1, 5, 0)]
         ClusterGroupGetNodeIds = 5100,
-        [MinVersion(1, 5, 0)]
-        ClusterGroupGetNodesInfo = 5101
+        ClusterGroupGetNodesInfo = 5101,
+        ClusterGroupGetNodesEndpoints = 5102
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOpExtensions.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOpExtensions.cs
deleted file mode 100644
index c50ce26f..0000000
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOpExtensions.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.Core.Impl.Client
-{
-    using System;
-    using System.Collections.Generic;
-    using System.Linq;
-
-    /// <summary>
-    /// Extension methods for <see cref="ClientOp"/>.
-    /// </summary>
-    internal static class ClientOpExtensions
-    {
-        /** */
-        private static readonly Dictionary<ClientOp, ClientProtocolVersion> VersionMap = GetVersionMap();
-
-        /// <summary>
-        /// Gets minimum protocol version that is required to perform specified operation.
-        /// </summary>
-        /// <param name="op">Operation.</param>
-        /// <returns>Minimum protocol version.</returns>
-        public static ClientProtocolVersion GetMinVersion(this ClientOp op)
-        {
-            ClientProtocolVersion minVersion;
-            
-            return VersionMap.TryGetValue(op, out minVersion) 
-                ? minVersion 
-                : ClientSocket.Ver100;
-        }
-        
-        /// <summary>
-        /// Gets the version map.
-        /// </summary>
-        private static Dictionary<ClientOp, ClientProtocolVersion> GetVersionMap()
-        {
-            var res = new Dictionary<ClientOp, ClientProtocolVersion>();
-            
-            foreach (var memberInfo in typeof(ClientOp).GetMembers())
-            {
-                var attr = memberInfo.GetCustomAttributes(false)
-                    .OfType<MinVersionAttribute>()
-                    .SingleOrDefault();
-
-                if (attr == null)
-                {
-                    continue;
-                }
-
-                var clientOp = (ClientOp) Enum.Parse(typeof(ClientOp), memberInfo.Name);
-                res[clientOp] = attr.Version;
-            }
-
-            return res;
-        }
-    }
-}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs
index d291136..dbd53e2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientRequestContext.cs
@@ -33,9 +33,9 @@ namespace Apache.Ignite.Core.Impl.Client
         /// </summary>
         /// <param name="stream">Stream.</param>
         /// <param name="marshaller">Marshaller.</param>
-        /// <param name="protocolVersion">Protocol version to be used for this request.</param>
-        public ClientRequestContext(IBinaryStream stream, Marshaller marshaller, ClientProtocolVersion protocolVersion)
-            : base(stream, marshaller, protocolVersion)
+        /// <param name="features">Features supported by this request.</param>
+        public ClientRequestContext(IBinaryStream stream, Marshaller marshaller, ClientFeatures features)
+            : base(stream, marshaller, features)
 
         {
             // No-op.
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientResponseContext.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientResponseContext.cs
index 1ef1419..91b09ea 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientResponseContext.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientResponseContext.cs
@@ -33,9 +33,9 @@ namespace Apache.Ignite.Core.Impl.Client
         /// </summary>
         /// <param name="stream">Stream.</param>
         /// <param name="marshaller">Marshaller.</param>
-        /// <param name="protocolVersion">Protocol version to be used for this response.</param>
-        public ClientResponseContext(IBinaryStream stream, Marshaller marshaller, ClientProtocolVersion protocolVersion)
-            : base(stream, marshaller, protocolVersion)
+        /// <param name="features">Features supported by this request.</param>
+        public ClientResponseContext(IBinaryStream stream, Marshaller marshaller, ClientFeatures features)
+            : base(stream, marshaller, features)
         {
             // No-op.
         }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs
index 595ade9..09e7095 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs
@@ -18,6 +18,7 @@
 namespace Apache.Ignite.Core.Impl.Client
 {
     using System;
+    using System.Collections;
     using System.Collections.Concurrent;
     using System.Diagnostics;
     using System.Diagnostics.CodeAnalysis;
@@ -87,9 +88,6 @@ namespace Apache.Ignite.Core.Impl.Client
         /** Callback checker guard. */
         private volatile bool _checkingTimeouts;
 
-        /** Server protocol version. */
-        public ClientProtocolVersion ServerVersion { get; private set; }
-
         /** Current async operations, map from request id. */
         private readonly ConcurrentDictionary<long, Request> _requests
             = new ConcurrentDictionary<long, Request>();
@@ -121,6 +119,9 @@ namespace Apache.Ignite.Core.Impl.Client
         /** Marshaller. */
         private readonly Marshaller _marsh;
 
+        /** Features. */
+        private readonly ClientFeatures _features;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ClientSocket" /> class.
         /// </summary>
@@ -152,7 +153,7 @@ namespace Apache.Ignite.Core.Impl.Client
 
             Validate(clientConfiguration);
 
-            Handshake(clientConfiguration, ServerVersion);
+            _features = Handshake(clientConfiguration, ServerVersion);
 
             // Check periodically if any request has timed out.
             if (_timeout > TimeSpan.Zero)
@@ -227,6 +228,14 @@ namespace Apache.Ignite.Core.Impl.Client
         }
 
         /// <summary>
+        /// Gets the features.
+        /// </summary>
+        public ClientFeatures Features
+        {
+            get { return _features; }
+        }
+        
+        /// <summary>
         /// Gets the current remote EndPoint.
         /// </summary>
         public EndPoint RemoteEndPoint { get { return _socket.RemoteEndPoint; } }
@@ -250,6 +259,11 @@ namespace Apache.Ignite.Core.Impl.Client
         }
 
         /// <summary>
+        /// Gets the server protocol version.
+        /// </summary>
+        public ClientProtocolVersion ServerVersion { get; private set; }
+
+        /// <summary>
         /// Starts waiting for the new message.
         /// </summary>
         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
@@ -347,7 +361,7 @@ namespace Apache.Ignite.Core.Impl.Client
             if (statusCode == ClientStatusCode.Success)
             {
                 return readFunc != null 
-                    ? readFunc(new ClientResponseContext(stream, _marsh, ServerVersion)) 
+                    ? readFunc(new ClientResponseContext(stream, _marsh, _features)) 
                     : default(T);
             }
 
@@ -364,10 +378,10 @@ namespace Apache.Ignite.Core.Impl.Client
         /// <summary>
         /// Performs client protocol handshake.
         /// </summary>
-        private void Handshake(IgniteClientConfiguration clientConfiguration, ClientProtocolVersion version)
+        private ClientFeatures Handshake(IgniteClientConfiguration clientConfiguration, ClientProtocolVersion version)
         {
-            bool auth = version >= Ver110 && clientConfiguration.UserName != null;
-            bool features = version >= Ver170;
+            var hasAuth = version >= Ver110 && clientConfiguration.UserName != null;
+            var hasFeatures = version >= Ver170;
 
             // Send request.
             int messageLen;
@@ -385,16 +399,14 @@ namespace Apache.Ignite.Core.Impl.Client
                 stream.WriteByte(ClientType);
 
                 // Writing features.
-                if (features)
+                if (hasFeatures)
                 {
-                    // TODO: Implement client-side features.
-                    var featureBytes = new byte[0];
-
-                    BinaryUtils.Marshaller.Marshal(stream, w => w.WriteByteArray(featureBytes));
+                    BinaryUtils.Marshaller.Marshal(stream, 
+                        w => w.WriteByteArray(ClientFeatures.AllFeatures));
                 }
 
                 // Authentication data.
-                if (auth)
+                if (hasAuth)
                 {
                     BinaryUtils.Marshaller.Marshal(stream, writer =>
                     {
@@ -416,10 +428,11 @@ namespace Apache.Ignite.Core.Impl.Client
 
                 if (success)
                 {
-                    if (version >= Ver170)
+                    BitArray featureBits = null;
+                    
+                    if (hasFeatures)
                     {
-                        // TODO: Implement features support
-                        BinaryUtils.Marshaller.Unmarshal<byte[]>(stream);
+                        featureBits = new BitArray(BinaryUtils.Marshaller.Unmarshal<byte[]>(stream));
                     }
 
                     if (version >= Ver140)
@@ -432,7 +445,7 @@ namespace Apache.Ignite.Core.Impl.Client
                     _logger.Debug("Handshake completed on {0}, protocol version = {1}", 
                         _socket.RemoteEndPoint, version);
 
-                    return;
+                    return new ClientFeatures(version, featureBits);
                 }
 
                 ServerVersion =
@@ -467,14 +480,12 @@ namespace Apache.Ignite.Core.Impl.Client
                     _logger.Debug("Retrying handshake on {0} with protocol version {1}", 
                         _socket.RemoteEndPoint, ServerVersion);
                     
-                    Handshake(clientConfiguration, ServerVersion);
-                }
-                else
-                {
-                    throw new IgniteClientException(string.Format(
-                        "Client handshake failed: '{0}'. Client version: {1}. Server version: {2}",
-                        errMsg, version, ServerVersion), null, errCode);
+                    return Handshake(clientConfiguration, ServerVersion);
                 }
+
+                throw new IgniteClientException(string.Format(
+                    "Client handshake failed: '{0}'. Client version: {1}. Server version: {2}",
+                    errMsg, version, ServerVersion), null, errCode);
             }
         }
 
@@ -606,7 +617,7 @@ namespace Apache.Ignite.Core.Impl.Client
         /// </summary>
         private RequestMessage WriteMessage(Action<ClientRequestContext> writeAction, ClientOp opId)
         {
-            ClientUtils.ValidateOp(opId, ServerVersion);
+            _features.ValidateOp(opId);
             
             var requestId = Interlocked.Increment(ref _requestId);
             
@@ -621,7 +632,7 @@ namespace Apache.Ignite.Core.Impl.Client
 
             if (writeAction != null)
             {
-                var ctx = new ClientRequestContext(stream, _marsh, ServerVersion);
+                var ctx = new ClientRequestContext(stream, _marsh, _features);
                 writeAction(ctx);
                 ctx.FinishMarshal();
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientUtils.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientUtils.cs
deleted file mode 100644
index e9bec08..0000000
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientUtils.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.Core.Impl.Client
-{
-    using Apache.Ignite.Core.Client;
-
-    /// <summary>
-    /// Client utils.
-    /// </summary>
-    internal static class ClientUtils
-    {
-        /// <summary>
-        /// Validates op code against current protocol version.
-        /// </summary>
-        /// <param name="operation">Operation.</param>
-        /// <param name="protocolVersion">Protocol version.</param>
-        public static void ValidateOp(ClientOp operation, ClientProtocolVersion protocolVersion)
-        {
-            ValidateOp(operation, protocolVersion, operation.GetMinVersion());
-        }
-        
-        /// <summary>
-        /// Validates op code against current protocol version.
-        /// </summary>
-        /// <param name="operation">Operation.</param>
-        /// <param name="protocolVersion">Protocol version.</param>
-        /// <param name="requiredProtocolVersion">Required protocol version.</param>
-        public static void ValidateOp<T>(T operation, ClientProtocolVersion protocolVersion, 
-            ClientProtocolVersion requiredProtocolVersion)
-        {
-            if (protocolVersion >= requiredProtocolVersion)
-            {
-                return;
-            }
-
-            var message = string.Format("Operation {0} is not supported by protocol version {1}. " +
-                                        "Minimum protocol version required is {2}.", 
-                operation, protocolVersion, requiredProtocolVersion);
-                
-            throw new IgniteClientException(message);
-        }
-    }
-}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Endpoint.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Endpoint.cs
index a42d6cd..1d56b6d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Endpoint.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/Endpoint.cs
@@ -29,7 +29,7 @@ namespace Apache.Ignite.Core.Impl.Client
     internal class Endpoint
     {
         /** */
-        private static readonly string[] HostSeparators = {":"};
+        private const char HostSeparator = ':';
 
         /** */
         private static readonly string[] PortsSeparators = {".."};
@@ -84,7 +84,7 @@ namespace Apache.Ignite.Core.Impl.Client
         /// <summary>
         /// Parses the endpoint string.
         /// </summary>
-        private static Endpoint ParseEndpoint(string endpoint)
+        public static Endpoint ParseEndpoint(string endpoint)
         {
             if (string.IsNullOrWhiteSpace(endpoint))
             {
@@ -92,38 +92,35 @@ namespace Apache.Ignite.Core.Impl.Client
                     "IgniteClientConfiguration.Endpoints[...] can't be null or whitespace.");
             }
 
-            var parts = endpoint.Split(HostSeparators, StringSplitOptions.None);
+            var idx = endpoint.LastIndexOf(HostSeparator);
 
-            if (parts.Length == 1)
+            if (idx == -1)
             {
                 return new Endpoint(endpoint);
             }
 
-            if (parts.Length == 2)
+            var host = endpoint.Substring(0, idx);
+            var port = endpoint.Substring(idx + 1);
+
+            var ports = port.Split(PortsSeparators, StringSplitOptions.None);
+
+            if (ports.Length == 1)
             {
-                var host = parts[0];
-                var port = parts[1];
+                return new Endpoint(host, ParsePort(endpoint, port));
+            }
 
-                var ports = port.Split(PortsSeparators, StringSplitOptions.None);
+            if (ports.Length == 2)
+            {
+                var minPort = ParsePort(endpoint, ports[0]);
+                var maxPort = ParsePort(endpoint, ports[1]);
 
-                if (ports.Length == 1)
+                if (maxPort < minPort)
                 {
-                    return new Endpoint(host, ParsePort(endpoint, port));
+                    throw new IgniteClientException(
+                        "Invalid format of IgniteClientConfiguration.Endpoint, port range is empty: " + endpoint);
                 }
 
-                if (ports.Length == 2)
-                {
-                    var minPort = ParsePort(endpoint, ports[0]);
-                    var maxPort = ParsePort(endpoint, ports[1]);
-
-                    if (maxPort < minPort)
-                    {
-                        throw new IgniteClientException(
-                            "Invalid format of IgniteClientConfiguration.Endpoint, port range is empty: " + endpoint);
-                    }
-
-                    return new Endpoint(host, minPort, maxPort - minPort);
-                }
+                return new Endpoint(host, minPort, maxPort - minPort);
             }
 
             throw new IgniteClientException("Unrecognized format of IgniteClientConfiguration.Endpoint: " + endpoint);
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/IgniteClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/IgniteClient.cs
index 7d57c0d..4f1100b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/IgniteClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/IgniteClient.cs
@@ -62,6 +62,9 @@ namespace Apache.Ignite.Core.Impl.Client
         /** Node info cache. */
         private readonly ConcurrentDictionary<Guid, IClientClusterNode> _nodes =
             new ConcurrentDictionary<Guid, IClientClusterNode>();
+        
+        /** Cluster. */
+        private readonly ClientCluster _cluster;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="IgniteClient"/> class.
@@ -83,6 +86,8 @@ namespace Apache.Ignite.Core.Impl.Client
             _binProc = _configuration.BinaryProcessor ?? new BinaryProcessorClient(_socket);
 
             _binary = new Binary(_marsh);
+            
+            _cluster = new ClientCluster(this, _marsh);
         }
 
         /// <summary>
@@ -125,7 +130,7 @@ namespace Apache.Ignite.Core.Impl.Client
             IgniteArgumentCheck.NotNull(configuration, "configuration");
 
             DoOutOp(ClientOp.CacheGetOrCreateWithConfiguration,
-                ctx => ClientCacheConfigurationSerializer.Write(ctx.Stream, configuration, ctx.ProtocolVersion));
+                ctx => ClientCacheConfigurationSerializer.Write(ctx.Stream, configuration, ctx.Features));
 
             return GetCache<TK, TV>(configuration.Name);
         }
@@ -146,7 +151,7 @@ namespace Apache.Ignite.Core.Impl.Client
             IgniteArgumentCheck.NotNull(configuration, "configuration");
 
             DoOutOp(ClientOp.CacheCreateWithConfiguration,
-                ctx => ClientCacheConfigurationSerializer.Write(ctx.Stream, configuration, ctx.ProtocolVersion));
+                ctx => ClientCacheConfigurationSerializer.Write(ctx.Stream, configuration, ctx.Features));
 
             return GetCache<TK, TV>(configuration.Name);
         }
@@ -160,7 +165,7 @@ namespace Apache.Ignite.Core.Impl.Client
         /** <inheritDoc /> */
         public IClientCluster GetCluster()
         {
-            return new ClientCluster(this, _marsh);
+            return _cluster;
         }
 
         /** <inheritDoc /> */
@@ -221,6 +226,12 @@ namespace Apache.Ignite.Core.Impl.Client
         }
 
         /** <inheritDoc /> */
+        public IEnumerable<IClientConnection> GetConnections()
+        {
+            return _socket.GetConnections();
+        }
+
+        /** <inheritDoc /> */
         public IBinaryProcessor BinaryProcessor
         {
             get { return _binProc; }
diff --git a/modules/platforms/dotnet/DEVNOTES.txt b/modules/platforms/dotnet/DEVNOTES.txt
index 3e632fe..dcfaaf7 100644
--- a/modules/platforms/dotnet/DEVNOTES.txt
+++ b/modules/platforms/dotnet/DEVNOTES.txt
@@ -50,9 +50,9 @@ Getting started:
   ./build.sh
 * Run tests:
   cd Apache.Ignite.Core.Tests.DotNetCore
-  dotnet test --logger "console;verbosity=normal"
+  dotnet test --logger "console;verbosity=detailed"
 * Run specific test:
-  dotnet test --filter CacheTest --logger "console;verbosity=normal"
+  dotnet test --filter CacheTest --logger "console;verbosity=detailed"
 * IDE: Open Apache.Ignite.DotNetCore.sln
 
 


Mime
View raw message