You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2022/08/03 07:09:58 UTC

[ignite] branch master updated: IGNITE-17206 .NET: Thin client: Add IgniteSet (#10177)

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 d7c307a6dd1 IGNITE-17206 .NET: Thin client: Add IgniteSet (#10177)
d7c307a6dd1 is described below

commit d7c307a6dd1b0f1874b507cf231347672ec871bb
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Wed Aug 3 10:09:49 2022 +0300

    IGNITE-17206 .NET: Thin client: Add IgniteSet (#10177)
    
    * Implement `IgniteSet.withKeepBinary` to support non-Java user classes.
    * Add `IIgniteSetClient`, implement all standard `ISet` methods except `IsSubsetOf`, `IsProperSubsetOf`, `Overlaps`, `SymmetricExceptWith` (do not map to Java API).
---
 .../src/main/java/org/apache/ignite/IgniteSet.java |   8 +
 .../datastructures/CacheDataStructuresManager.java |   2 +-
 .../datastructures/GridCacheSetImpl.java           |  33 ++-
 .../datastructures/GridCacheSetProxy.java          |   5 +
 .../datastructures/ClientIgniteSetRequest.java     |   8 +-
 .../Client/Cache/PartitionAwarenessTest.cs         |  65 +++++
 .../Client/DataStructures/IgniteSetClientTests.cs  | 308 +++++++++++++++++++++
 .../IgniteSetClientTestsColocated.cs               |  37 +++
 .../CollectionClientConfiguration.cs               |  65 +++++
 .../Client/DataStructures/IIgniteSetClient.cs      |  63 +++++
 .../Apache.Ignite.Core/Client/IIgniteClient.cs     |  10 +
 .../Apache.Ignite.Core/Impl/Client/ClientOp.cs     |  17 +-
 .../Impl/Client/DataStructures/IgniteSetClient.cs  | 293 ++++++++++++++++++++
 .../DataStructures/IgniteSetClientEnumerator.cs    | 151 ++++++++++
 .../Apache.Ignite.Core/Impl/Client/IgniteClient.cs |  39 +++
 15 files changed, 1091 insertions(+), 13 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteSet.java b/modules/core/src/main/java/org/apache/ignite/IgniteSet.java
index 25d55efd8f7..1ec95fbff67 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteSet.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteSet.java
@@ -130,4 +130,12 @@ public interface IgniteSet<T> extends Set<T>, Closeable {
      * @throws IgniteException If job failed.
      */
     public <R> R affinityCall(IgniteCallable<R> job) throws IgniteException;
+
+    /**
+     * Returns Ignite set that operates on binary objects without deserialization.
+     *
+     * @param <T1> Type of binary objects.
+     * @return New set instance for binary objects.
+     */
+    public <T1> IgniteSet<T1> withKeepBinary();
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/datastructures/CacheDataStructuresManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/datastructures/CacheDataStructuresManager.java
index 271bbe127fe..abf7881a7c7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/datastructures/CacheDataStructuresManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/datastructures/CacheDataStructuresManager.java
@@ -403,7 +403,7 @@ public class CacheDataStructuresManager extends GridCacheManagerAdapter {
 
             if (set == null) {
                 GridCacheSetProxy<T> old = setsMap.putIfAbsent(hdr.id(),
-                    set = new GridCacheSetProxy<>(cctx, new GridCacheSetImpl<T>(cctx, name, hdr)));
+                    set = new GridCacheSetProxy<>(cctx, new GridCacheSetImpl<T>(cctx, name, hdr, false)));
 
                 if (old != null)
                     set = old;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetImpl.java
index 5965df81df7..6417736f3f1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetImpl.java
@@ -99,13 +99,19 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
     /** Access to affinityRun() and affinityCall() functions. */
     private final IgniteCompute compute;
 
+    /** */
+    private final boolean keepBinary;
+
+    /** */
+    private final GridCacheSetHeader hdr;
+
     /**
      * @param ctx Cache context.
      * @param name Set name.
      * @param hdr Set header.
      */
     @SuppressWarnings("unchecked")
-    public GridCacheSetImpl(GridCacheContext ctx, String name, GridCacheSetHeader hdr) {
+    public GridCacheSetImpl(GridCacheContext ctx, String name, GridCacheSetHeader hdr, boolean keepBinary) {
         this.ctx = ctx;
         this.name = name;
         this.collocated = hdr.collocated();
@@ -116,6 +122,8 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
         this.log = ctx.logger(GridCacheSetImpl.class);
         this.hdrPart = ctx.affinity().partition(setKey);
         this.separated = hdr.separated();
+        this.hdr = hdr;
+        this.keepBinary = keepBinary;
     }
 
     /** {@inheritDoc} */
@@ -304,7 +312,7 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
         try {
             onAccess();
 
-            try (GridCloseableIterator<T> iter = iterator0()) {
+            try (GridCloseableIterator<T> iter = iterator0(keepBinary)) {
                 boolean rmv = false;
 
                 Set<SetItemKey> rmvKeys = null;
@@ -342,7 +350,7 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
         try {
             onAccess();
 
-            try (GridCloseableIterator<T> iter = iterator0()) {
+            try (GridCloseableIterator<T> iter = iterator0(keepBinary)) {
                 Collection<SetItemKey> rmvKeys = new ArrayList<>(BATCH_SIZE);
 
                 for (T val : iter) {
@@ -368,7 +376,7 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
     @Override public Iterator<T> iterator() {
         onAccess();
 
-        return iterator0();
+        return iterator0(keepBinary);
     }
 
     /** {@inheritDoc} */
@@ -389,6 +397,11 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
         return compute.affinityCall(cache.name(), setKey, job);
     }
 
+    /** {@inheritDoc} */
+    @Override public <T1> IgniteSet<T1> withKeepBinary() {
+        return new GridCacheSetImpl<>(ctx, name, hdr, true);
+    }
+
     /** {@inheritDoc} */
     @Override public void close() {
         try {
@@ -405,9 +418,9 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
     /**
      * @return Closeable iterator.
      */
-    private GridCloseableIterator<T> iterator0() {
+    private GridCloseableIterator<T> iterator0(boolean keepBinary) {
         try {
-            WeakReferenceCloseableIterator<T> it = separated ? separatedCacheIterator() : sharedCacheIterator();
+            WeakReferenceCloseableIterator<T> it = separated ? separatedCacheIterator(keepBinary) : sharedCacheIterator(keepBinary);
 
             if (rmvd) {
                 ctx.itHolder().removeIterator(it);
@@ -426,10 +439,10 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
      * @return Shared cache iterator.
      */
     @SuppressWarnings("unchecked")
-    private WeakReferenceCloseableIterator<T> sharedCacheIterator() throws IgniteCheckedException {
+    private WeakReferenceCloseableIterator<T> sharedCacheIterator(boolean keepBinary) throws IgniteCheckedException {
         CacheQuery qry = new GridCacheQueryAdapter<>(ctx, SET, null, null,
             new GridSetQueryPredicate<>(id, collocated), collocated ? hdrPart : null,
-            false, false, null);
+            false, keepBinary, null);
 
         Collection<ClusterNode> nodes = dataNodes(ctx.affinity().affinityTopologyVersion());
 
@@ -452,9 +465,9 @@ public class GridCacheSetImpl<T> extends AbstractCollection<T> implements Ignite
      * @return Separated cache iterator.
      */
     @SuppressWarnings("unchecked")
-    private WeakReferenceCloseableIterator<T> separatedCacheIterator() throws IgniteCheckedException {
+    private WeakReferenceCloseableIterator<T> separatedCacheIterator(boolean keepBinary) throws IgniteCheckedException {
         GridCloseableIterator iter =
-            (GridCloseableIterator)cache.scanIterator(false, new IgniteBiPredicate<Object, Object>() {
+            (GridCloseableIterator)cache.scanIterator(keepBinary, new IgniteBiPredicate<Object, Object>() {
                 @Override public boolean apply(Object k, Object v) {
                     return k.getClass() == GridCacheSetItemKey.class;
                 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetProxy.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetProxy.java
index a1cb56b301c..e7be06cc349 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetProxy.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/datastructures/GridCacheSetProxy.java
@@ -415,6 +415,11 @@ public class GridCacheSetProxy<T> implements IgniteSet<T>, Externalizable {
         return delegate.affinityCall(job);
     }
 
+    /** {@inheritDoc} */
+    @Override public <T1> IgniteSet<T1> withKeepBinary() {
+        return new GridCacheSetProxy<>(cctx, (GridCacheSetImpl<T1>)delegate.<T1>withKeepBinary());
+    }
+
     /**
      * Enters busy state.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/datastructures/ClientIgniteSetRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/datastructures/ClientIgniteSetRequest.java
index 988656b2543..629a9f94b7d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/datastructures/ClientIgniteSetRequest.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/datastructures/ClientIgniteSetRequest.java
@@ -87,7 +87,13 @@ public class ClientIgniteSetRequest extends ClientRequest {
      */
     protected <T> IgniteSet<T> igniteSet(ClientConnectionContext ctx) {
         // Thin client only works in separated mode, because non-separated mode was discontinued earlier.
-        return ctx.kernalContext().grid().set(name, cacheId, collocated, true);
+        IgniteSet<T> set = ctx.kernalContext().grid().set(name, cacheId, collocated, true);
+
+        if (set != null) {
+            set = set.withKeepBinary();
+        }
+
+        return set;
     }
 
     /**
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 bf06b57c5f1..d6adb4929ad 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
@@ -489,6 +489,71 @@ namespace Apache.Ignite.Core.Tests.Client.Cache
             Assert.AreEqual(gridIdx, GetClientRequestGridIndex("ValueGet", "datastructures.ClientAtomicLong"));
         }
 
+        [Test]
+        [TestCase("default-grp-partitioned-set", null, CacheMode.Partitioned, 1, 1)]
+        [TestCase("default-grp-partitioned-set", null, CacheMode.Partitioned, 2, 0)]
+        [TestCase("default-grp-partitioned-set", null, CacheMode.Partitioned, 3, 0)]
+        [TestCase("default-grp-partitioned-set", null, CacheMode.Partitioned, 4, 1)]
+        [TestCase("custom-grp-partitioned-set", "testIgniteSet1", CacheMode.Partitioned, 1, 1)]
+        [TestCase("custom-grp-partitioned-set", "testIgniteSet1", CacheMode.Partitioned, 3, 0)]
+        [TestCase("custom-grp-replicated-set", "testIgniteSet2", CacheMode.Replicated, 1, 1)]
+        [TestCase("custom-grp-replicated-set", "testIgniteSet2", CacheMode.Replicated, 3, 1)]
+        public void IgniteSet_RequestIsRoutedToPrimaryNode(
+            string name, string groupName, CacheMode cacheMode, int item, int gridIdx)
+        {
+            var cfg = new CollectionClientConfiguration
+            {
+                GroupName = groupName,
+                CacheMode = cacheMode,
+                Backups = 1
+            };
+
+            var igniteSet = Client.GetIgniteSet<int>(name, cfg);
+
+            // Warm up.
+            igniteSet.Add(0);
+            ClearLoggers();
+
+            // Test.
+            igniteSet.Add(item);
+
+            Assert.AreEqual(gridIdx, GetClientRequestGridIndex("ValueAdd", "datastructures.ClientIgniteSet"));
+        }
+
+        [Test]
+        [TestCase("default-grp-partitioned-set-2", null, CacheMode.Partitioned, 1, 0)]
+        [TestCase("default-grp-partitioned-set-2", null, CacheMode.Partitioned, 2, 0)]
+        [TestCase("default-grp-partitioned-set-2", null, CacheMode.Partitioned, 3, 0)]
+        [TestCase("default-grp-partitioned-set-2", null, CacheMode.Partitioned, 4, 0)]
+        [TestCase("custom-grp-partitioned-set-2", "testIgniteSetColocated1", CacheMode.Partitioned, 1, 1)]
+        [TestCase("custom-grp-partitioned-set-2", "testIgniteSetColocated1", CacheMode.Partitioned, 2, 1)]
+        [TestCase("custom-grp-partitioned-set-2", "testIgniteSetColocated1", CacheMode.Partitioned, 3, 1)]
+        [TestCase("custom-grp-replicated-set-2", "testIgniteSetColocated2", CacheMode.Replicated, 1, 1)]
+        [TestCase("custom-grp-replicated-set-2", "testIgniteSetColocated2", CacheMode.Replicated, 2, 1)]
+        [TestCase("custom-grp-replicated-set-2", "testIgniteSetColocated2", CacheMode.Replicated, 3, 1)]
+        public void IgniteSetColocated_RequestIsRoutedToPrimaryNode(
+            string name, string groupName, CacheMode cacheMode, int item, int gridIdx)
+        {
+            var cfg = new CollectionClientConfiguration
+            {
+                GroupName = groupName,
+                CacheMode = cacheMode,
+                Colocated = true,
+                Backups = 1
+            };
+
+            var igniteSet = Client.GetIgniteSet<int>(name, cfg);
+
+            // Warm up.
+            igniteSet.Add(0);
+            ClearLoggers();
+
+            // Test.
+            igniteSet.Add(1);
+
+            Assert.AreEqual(gridIdx, GetClientRequestGridIndex("ValueAdd", "datastructures.ClientIgniteSet"));
+        }
+
         protected override IgniteClientConfiguration GetClientConfiguration()
         {
             var cfg = base.GetClientConfiguration();
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/DataStructures/IgniteSetClientTests.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/DataStructures/IgniteSetClientTests.cs
new file mode 100644
index 00000000000..7fb15658a91
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/DataStructures/IgniteSetClientTests.cs
@@ -0,0 +1,308 @@
+/*
+ * 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.DataStructures
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Reflection;
+    using Apache.Ignite.Core.Cache.Configuration;
+    using Apache.Ignite.Core.Client;
+    using Apache.Ignite.Core.Client.DataStructures;
+    using Apache.Ignite.Core.Tests.Client.Cache;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="IIgniteSetClient{T}"/>.
+    /// Partition awareness tests: <see cref="PartitionAwarenessTest.IgniteSet_RequestIsRoutedToPrimaryNode"/>,
+    /// <see cref="PartitionAwarenessTest.IgniteSetColocated_RequestIsRoutedToPrimaryNode"/>
+    /// </summary>
+    public class IgniteSetClientTests : ClientTestBase
+    {
+        [Test]
+        public void TestCreateAddContainsRemove()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestCreateAddContainsRemove),
+                GetCollectionConfiguration());
+
+            set.Add(1);
+            set.Add(1);
+
+            set.Add(2);
+            set.Add(2);
+
+            set.Remove(1);
+
+            Assert.AreEqual(nameof(TestCreateAddContainsRemove), set.Name);
+            Assert.IsTrue(set.Contains(2));
+            Assert.IsFalse(set.Contains(1));
+            Assert.AreEqual(1, set.Count);
+            Assert.AreEqual(2, set.Single());
+            Assert.IsFalse(set.IsClosed);
+        }
+
+        [Test]
+        public void TestEnumeratorEmptySet()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestEnumeratorEmptySet),
+                GetCollectionConfiguration());
+
+            using var enumerator = set.GetEnumerator();
+
+            Assert.Throws<InvalidOperationException>(() => _ = enumerator.Current);
+            Assert.IsFalse(enumerator.MoveNext());
+            Assert.Throws<InvalidOperationException>(() => _ = enumerator.Current);
+        }
+
+        [Test]
+        public void TestEnumeratorMultiplePages()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestEnumeratorMultiplePages),
+                GetCollectionConfiguration());
+
+            set.PageSize = 2;
+
+            var items = Enumerable.Range(1, 10).ToList();
+            items.ForEach(x => set.Add(x));
+
+            var res = set.ToList();
+
+            CollectionAssert.AreEquivalent(items, res);
+        }
+
+        [Test]
+        public void TestEnumeratorRemoveAllWhileEnumerating()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestEnumeratorRemoveAllWhileEnumerating),
+                new CollectionClientConfiguration());
+
+            set.PageSize = 2;
+
+            var items = Enumerable.Range(1, 10).ToList();
+            items.ForEach(x => set.Add(x));
+
+            using var enumerator = set.GetEnumerator();
+
+            items.ForEach(x => set.Remove(x));
+
+            var res = new List<int>();
+
+            while (enumerator.MoveNext())
+            {
+                res.Add(enumerator.Current);
+            }
+
+            // 2 from current page, 1 from current item cached on server.
+            Assert.AreEqual(3, res.Count);
+        }
+
+        [Test]
+        public void TestClear()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestClear), GetCollectionConfiguration());
+
+            set.UnionWith(new[] { 1, 2 });
+            set.Clear();
+
+            Assert.AreEqual(0, set.Count);
+            Assert.IsFalse(set.Contains(1));
+            Assert.IsFalse(set.Contains(2));
+        }
+
+        [Test]
+        public void TestExceptWith()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestExceptWith), GetCollectionConfiguration());
+
+            set.UnionWith(new[] { 1, 2, 3 });
+            set.ExceptWith(new[] { 1, 3, 5, 7 });
+            set.ExceptWith(Array.Empty<int>());
+
+            Assert.AreEqual(2, set.Single());
+        }
+
+        [Test]
+        public void TestIntersectWith()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestIntersectWith), GetCollectionConfiguration());
+
+            set.UnionWith(new[] { 1, 2, 3 });
+            set.IntersectWith(new[] { 1, 3, 5 });
+
+            CollectionAssert.AreEquivalent(new[] { 1, 3 }, set);
+        }
+
+        [Test]
+        public void TestGetIgniteSetNonExistingReturnsNull()
+        {
+            Assert.IsNull(Client.GetIgniteSet<int>("foobar", null));
+        }
+
+        [Test]
+        public void TestClose()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestClose), GetCollectionConfiguration());
+
+            set.Add(1);
+            set.Close();
+
+            Assert.IsTrue(set.IsClosed);
+
+            var ex = Assert.Throws<IgniteClientException>(() => set.Add(2));
+            Assert.AreEqual("IgniteSet with name 'TestClose' does not exist.", ex.Message);
+        }
+
+        [Test]
+        public void TestConfigurationPropagation()
+        {
+            var cfg = new CollectionClientConfiguration
+            {
+                Backups = 7,
+                Colocated = false,
+                AtomicityMode = CacheAtomicityMode.Transactional,
+                CacheMode = CacheMode.Partitioned,
+                GroupName = "g-" + nameof(TestConfigurationPropagation)
+            };
+
+            var set = Client.GetIgniteSet<int>(nameof(TestConfigurationPropagation), cfg);
+
+            const string cacheName = "datastructures_TRANSACTIONAL_PARTITIONED_7@" +
+                                     "g-TestConfigurationPropagation#SET_TestConfigurationPropagation";
+
+            var cache = Client.GetCache<int, int>(cacheName);
+
+            Assert.AreEqual(
+                set.GetType().GetField("_cacheId", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(set),
+                cache.GetType().GetField("_id", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(cache));
+        }
+
+        [Test]
+        public void TestCopyTo()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestCopyTo), GetCollectionConfiguration());
+            set.UnionWith(new[] { 1, 2, 3 });
+
+            var target1 = new int[3];
+            set.CopyTo(target1, 0);
+
+            var target2 = new int[4];
+            set.CopyTo(target2, 1);
+
+            CollectionAssert.AreEquivalent(set, target1);
+            CollectionAssert.AreEquivalent(set, target2.Skip(1));
+        }
+
+        [Test]
+        public void TestCopyToInvalidArguments()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestCopyToInvalidArguments), GetCollectionConfiguration());
+            set.UnionWith(new[] { 1, 2, 3 });
+
+            var target1 = new int[3];
+
+            var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => set.CopyTo(target1, -1));
+            Assert.AreEqual("Non-negative number required. (Parameter 'arrayIndex')\nActual value was -1.",
+                ex1.Message);
+
+            var ex2 = Assert.Throws<ArgumentException>(() => set.CopyTo(target1, 1));
+            Assert.AreEqual("Destination array is not long enough to copy all the items in the collection. " +
+                            "Check array index and length. (Parameter 'array')", ex2.Message);
+
+            var ex3 = Assert.Throws<ArgumentException>(() => set.CopyTo(target1, 3));
+            Assert.AreEqual("Destination array is not long enough to copy all the items in the collection. " +
+                            "Check array index and length. (Parameter 'array')", ex3.Message);
+        }
+
+        [Test]
+        public void TestUserObjects()
+        {
+            var set = Client.GetIgniteSet<CustomClass>(nameof(TestUserObjects), GetCollectionConfiguration());
+
+            set.Add(new CustomClass { Id = 1, Name = "A" });
+            set.IntersectWith(new[] { new CustomClass { Id = 1, Name = "A" }, new CustomClass { Id = 2, Name = "B" } });
+
+            Assert.IsTrue(set.Contains(new CustomClass { Id = 1, Name = "A" }));
+            Assert.IsFalse(set.Contains(new CustomClass { Id = 1, Name = "a" }));
+
+            var res = set.Single();
+
+            Assert.AreEqual(1, res.Id);
+            Assert.AreEqual("A", res.Name);
+
+            set.Clear();
+            Assert.AreEqual(0, set.Count);
+        }
+
+        [Test]
+        public void TestIsSupersetOf()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestIsSupersetOf), GetCollectionConfiguration());
+            set.UnionWith(new[] { 1, 2, 3 });
+
+            Assert.IsTrue(set.IsSupersetOf(new[] { 3, 2, 1 }));
+            Assert.IsTrue(set.IsSupersetOf(new[] { 3, 2 }));
+            Assert.IsFalse(set.IsSupersetOf(new[] { 3, 0 }));
+        }
+
+        [Test]
+        public void TestIsProperSupersetOf()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestIsProperSupersetOf), GetCollectionConfiguration());
+            set.UnionWith(new[] { 1, 2, 3 });
+
+            Assert.IsFalse(set.IsProperSupersetOf(new[] { 3, 2, 1 }));
+            Assert.IsTrue(set.IsProperSupersetOf(new[] { 3, 2 }));
+            Assert.IsTrue(set.IsProperSupersetOf(new[] { 1 }));
+            Assert.IsFalse(set.IsProperSupersetOf(new[] { 3, 0 }));
+        }
+
+        [Test]
+        public void TestUnionWith()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestUnionWith), GetCollectionConfiguration());
+            set.UnionWith(new[] { 1 });
+            set.UnionWith(new[] { 1, 2 });
+            set.UnionWith(new[] { 0, 3 });
+
+            CollectionAssert.AreEquivalent(new[] { 0, 1, 2, 3 }, set);
+        }
+
+        [Test]
+        public void TestSetEquals()
+        {
+            var set = Client.GetIgniteSet<int>(nameof(TestSetEquals), GetCollectionConfiguration());
+            set.UnionWith(new[] { 1, 2, 3 });
+
+            Assert.IsTrue(set.SetEquals(new[] { 1, 2, 3 }));
+            Assert.IsTrue(set.SetEquals(new[] { 3, 1, 2 }));
+            Assert.IsFalse(set.SetEquals(new[] { 1, 2 }));
+            Assert.IsFalse(set.SetEquals(new[] { 1, 2, 4 }));
+            Assert.IsFalse(set.SetEquals(new[] { 1, 2, 3, 4 }));
+        }
+
+        protected virtual CollectionClientConfiguration GetCollectionConfiguration() =>
+            new CollectionClientConfiguration();
+
+        private class CustomClass
+        {
+            public int Id { get; set; }
+
+            public string Name { get; set; }
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/DataStructures/IgniteSetClientTestsColocated.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/DataStructures/IgniteSetClientTestsColocated.cs
new file mode 100644
index 00000000000..4076c8d9e17
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/DataStructures/IgniteSetClientTestsColocated.cs
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Core.Tests.Client.DataStructures
+{
+    using Apache.Ignite.Core.Client.DataStructures;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="IIgniteSetClient{T}"/> in <see cref="CollectionClientConfiguration.Colocated"/> mode.
+    /// </summary>
+    [TestFixture]
+    public class IgniteSetClientTestsColocated : IgniteSetClientTests
+    {
+        protected override CollectionClientConfiguration GetCollectionConfiguration()
+        {
+            var cfg = base.GetCollectionConfiguration();
+            cfg.Colocated = true;
+
+            return cfg;
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/DataStructures/CollectionClientConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/DataStructures/CollectionClientConfiguration.cs
new file mode 100644
index 00000000000..b0bb0183875
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/DataStructures/CollectionClientConfiguration.cs
@@ -0,0 +1,65 @@
+/*
+ * 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.Client.DataStructures
+{
+    using System.ComponentModel;
+    using Apache.Ignite.Core.Cache.Configuration;
+
+    /// <summary>
+    /// Configuration for Ignite collections.
+    /// </summary>
+    public sealed class CollectionClientConfiguration
+    {
+        /// <summary>
+        /// Default value for <see cref="CacheMode"/>.
+        /// </summary>
+        public const CacheMode DefaultCacheMode = CacheMode.Partitioned;
+
+        /// <summary>
+        /// Default value for <see cref="AtomicityMode"/>.
+        /// </summary>
+        public const CacheAtomicityMode DefaultAtomicityMode = CacheAtomicityMode.Atomic;
+
+        /// <summary>
+        /// Gets or sets the number of backup nodes for the underlying cache.
+        /// </summary>
+        public int Backups { get; set; }
+
+        /// <summary>
+        /// Gets or sets the cache mode for the underlying cache.
+        /// </summary>
+        [DefaultValue(DefaultCacheMode)]
+        public CacheMode CacheMode { get; set; } = DefaultCacheMode;
+
+        /// <summary>
+        /// Gets or sets the cache mode for the underlying cache.
+        /// </summary>
+        [DefaultValue(DefaultAtomicityMode)]
+        public CacheAtomicityMode AtomicityMode { get; set; } = DefaultAtomicityMode;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether all set items should be stored on a single node.
+        /// </summary>
+        public bool Colocated { get; set; }
+
+        /// <summary>
+        /// Gets or sets the cache group name.
+        /// </summary>
+        public string GroupName { get; set; }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/DataStructures/IIgniteSetClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/DataStructures/IIgniteSetClient.cs
new file mode 100644
index 00000000000..fb80ad17ac1
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/DataStructures/IIgniteSetClient.cs
@@ -0,0 +1,63 @@
+/*
+ * 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.Client.DataStructures
+{
+    using System.Collections.Generic;
+    using System.Diagnostics.CodeAnalysis;
+
+    /// <summary>
+    /// Distributed set: stores items on one or more Ignite cluster nodes.
+    /// <para />
+    /// Implements most of the methods from <see cref="ISet{T}"/>. The following methods are NOT supported:
+    /// <see cref="ISet{T}.IsSubsetOf"/>, <see cref="ISet{T}.IsProperSubsetOf"/>,
+    /// <see cref="ISet{T}.Overlaps"/>, <see cref="ISet{T}.SymmetricExceptWith"/>.
+    /// <para />
+    /// Set items can be placed on single node (when <see cref="CollectionClientConfiguration.Colocated"/> is true)
+    /// or distributed across grid nodes.
+    /// </summary>
+    /// <typeparam name="T">Item type.</typeparam>
+    [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
+    public interface IIgniteSetClient<T> : ISet<T>
+    {
+        /// <summary>
+        /// Gets the set name.
+        /// </summary>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether this set is colocated, that is, all items are stored on a single node.
+        /// </summary>
+        bool Colocated { get; }
+
+        /// <summary>
+        /// Gets or sets a value indicating the batch size for multi-item operations such as iteration.
+        /// </summary>
+        int PageSize { get; set; }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance was removed from the cluster.
+        /// </summary>
+        /// <returns>True if this set was removed; otherwise, false.</returns>
+        bool IsClosed { get; }
+
+        /// <summary>
+        /// Removes this set from the cluster.
+        /// </summary>
+        void Close();
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs
index cdb18dbbf31..9045414efde 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IIgniteClient.cs
@@ -221,5 +221,15 @@ namespace Apache.Ignite.Core.Client
             AtomicClientConfiguration configuration,
             long initialValue,
             bool create);
+
+        /// <summary>
+        /// Gets or creates an Ignite set with the specified name.
+        /// </summary>
+        /// <param name="name">Name.</param>
+        /// <param name="configuration">Configuration. When null, gets and existing set by name;
+        /// otherwise, creates a new set with the specified configuration.</param>
+        /// <typeparam name="T">Element type.</typeparam>
+        /// <returns>Ignite set.</returns>
+        IIgniteSetClient<T> GetIgniteSet<T>(string name, CollectionClientConfiguration configuration);
     }
 }
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 f19237c4638..b97c2c5e05b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientOp.cs
@@ -109,6 +109,21 @@ namespace Apache.Ignite.Core.Impl.Client
         AtomicLongValueAddAndGet = 9004,
         AtomicLongValueGetAndSet = 9005,
         AtomicLongValueCompareAndSet = 9006,
-        AtomicLongValueCompareAndSetAndGet = 9007
+        AtomicLongValueCompareAndSetAndGet = 9007,
+
+        SetGetOrCreate = 9010,
+        SetClose = 9011,
+        SetExists = 9012,
+        SetValueAdd = 9013,
+        SetValueAddAll = 9014,
+        SetValueRemove = 9015,
+        SetValueRemoveAll = 9016,
+        SetValueContains = 9017,
+        SetValueContainsAll = 9018,
+        SetValueRetainAll = 9019,
+        SetSize = 9020,
+        SetClear = 9021,
+        SetIteratorStart = 9022,
+        SetIteratorGetPage = 9023
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/DataStructures/IgniteSetClient.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/DataStructures/IgniteSetClient.cs
new file mode 100644
index 00000000000..ff044cd55fb
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/DataStructures/IgniteSetClient.cs
@@ -0,0 +1,293 @@
+/*
+ * 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.DataStructures
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using System.Diagnostics;
+    using System.IO;
+    using Apache.Ignite.Core.Client.DataStructures;
+    using Apache.Ignite.Core.Impl.Binary;
+    using Apache.Ignite.Core.Impl.Common;
+    using BinaryWriter = Apache.Ignite.Core.Impl.Binary.BinaryWriter;
+
+    /// <summary>
+    /// Client set.
+    /// </summary>
+    /// <typeparam name="T">Element type.</typeparam>
+    internal sealed class IgniteSetClient<T> : IIgniteSetClient<T>
+    {
+        /** */
+        private readonly ClientFailoverSocket _socket;
+
+        /** */
+        private readonly int _cacheId;
+
+        /** */
+        private readonly object _nameHash;
+
+        /** */
+        private int _pageSize = 1024;
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="IgniteSetClient{T}"/> class,
+        /// </summary>
+        /// <param name="socket">Socket.</param>
+        /// <param name="name">Set name.</param>
+        /// <param name="colocated">Colocated flag.</param>
+        /// <param name="cacheId">Cache id.</param>
+        public IgniteSetClient(ClientFailoverSocket socket, string name, bool colocated, int cacheId)
+        {
+            Debug.Assert(socket != null);
+            Debug.Assert(name != null);
+
+            _socket = socket;
+            _cacheId = cacheId;
+            _nameHash = BinaryUtils.GetStringHashCode(name);
+
+            Name = name;
+            Colocated = colocated;
+        }
+
+        /** <inheritdoc /> */
+        public IEnumerator<T> GetEnumerator()
+        {
+            if (Colocated)
+            {
+                return _socket.DoOutInOpAffinity(
+                    ClientOp.SetIteratorStart,
+                    ctx => WriteIteratorStart(ctx),
+                    ctx => new IgniteSetClientEnumerator<T>(ctx, PageSize),
+                    _cacheId,
+                    _nameHash);
+            }
+
+            return _socket.DoOutInOp(
+                ClientOp.SetIteratorStart,
+                ctx => WriteIteratorStart(ctx),
+                ctx => new IgniteSetClientEnumerator<T>(ctx, PageSize));
+        }
+
+        /** <inheritdoc /> */
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        /** <inheritdoc /> */
+        void ICollection<T>.Add(T item) => AddIfNotPresent(item);
+
+        /** <inheritdoc /> */
+        public void ExceptWith(IEnumerable<T> other) => MultiKeyOp(ClientOp.SetValueRemoveAll, other, out _);
+
+        /** <inheritdoc /> */
+        public void IntersectWith(IEnumerable<T> other) => MultiKeyOp(ClientOp.SetValueRetainAll, other, out _);
+
+        /** <inheritdoc /> */
+        public bool IsProperSubsetOf(IEnumerable<T> other) => throw new NotSupportedException();
+
+        /** <inheritdoc /> */
+        public bool IsProperSupersetOf(IEnumerable<T> other) =>
+            // If B is a proper superset of A, then all elements of A are in B
+            // but B contains at least one element that is not in A.
+            MultiKeyOp(ClientOp.SetValueContainsAll, other, out var otherCount) && Count > otherCount;
+
+        /** <inheritdoc /> */
+        public bool IsSubsetOf(IEnumerable<T> other) => throw new NotSupportedException();
+
+        /** <inheritdoc /> */
+        public bool IsSupersetOf(IEnumerable<T> other) => MultiKeyOp(ClientOp.SetValueContainsAll, other, out _);
+
+        /** <inheritdoc /> */
+        public bool Overlaps(IEnumerable<T> other) => throw new NotSupportedException();
+
+        /** <inheritdoc /> */
+        public bool SetEquals(IEnumerable<T> other) =>
+            MultiKeyOp(ClientOp.SetValueContainsAll, other, out var otherCount) && Count == otherCount;
+
+        /** <inheritdoc /> */
+        public void SymmetricExceptWith(IEnumerable<T> other) => throw new NotSupportedException();
+
+        /** <inheritdoc /> */
+        public void UnionWith(IEnumerable<T> other) => MultiKeyOp(ClientOp.SetValueAddAll, other, out _);
+
+        /** <inheritdoc /> */
+        bool ISet<T>.Add(T item) => AddIfNotPresent(item);
+
+        /** <inheritdoc /> */
+        public void Clear() => Op<object>(ClientOp.SetClear);
+
+        /** <inheritdoc /> */
+        public bool Contains(T item) => SingleKeyOp(ClientOp.SetValueContains, item);
+
+        /** <inheritdoc /> */
+        public void CopyTo(T[] array, int arrayIndex)
+        {
+            IgniteArgumentCheck.NotNull(array, nameof(array));
+
+            if (arrayIndex < 0)
+            {
+                // Here and below - same exception text as HashSet uses.
+                throw new ArgumentOutOfRangeException(
+                    paramName: nameof(arrayIndex),
+                    actualValue: arrayIndex,
+                    message: "Non-negative number required.");
+            }
+
+            foreach (var item in this)
+            {
+                if (arrayIndex >= array.Length)
+                {
+                    throw new ArgumentException(
+                        message: "Destination array is not long enough to copy all the items in the collection. " +
+                                 "Check array index and length.",
+                        paramName: nameof(array));
+                }
+
+                array[arrayIndex++] = item;
+            }
+        }
+
+        /** <inheritdoc /> */
+        public bool Remove(T item) => SingleKeyOp(ClientOp.SetValueRemove, item);
+
+        /** <inheritdoc /> */
+        public int Count => Op(ClientOp.SetSize, null, r => r.Stream.ReadInt());
+
+        /** <inheritdoc /> */
+        public bool IsReadOnly => false;
+
+        /** <inheritdoc /> */
+        public string Name { get; }
+
+        /** <inheritdoc /> */
+        public bool Colocated { get; }
+
+        /** <inheritdoc /> */
+        public int PageSize
+        {
+            get => _pageSize;
+            set => _pageSize = value > 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
+        }
+
+        /** <inheritdoc /> */
+        public bool IsClosed => Op(ClientOp.SetExists, null, r => !r.Stream.ReadBool());
+
+        /** <inheritdoc /> */
+        public void Close() => Op<object>(ClientOp.SetClose);
+
+        private bool AddIfNotPresent(T item) => SingleKeyOp(ClientOp.SetValueAdd, item);
+
+        private TRes Op<TRes>(
+            ClientOp op,
+            Action<BinaryWriter> writeAction = null,
+            Func<ClientResponseContext, TRes> readFunc = null) =>
+            _socket.DoOutInOp(op, ctx =>
+            {
+                WriteIdentity(ctx.Writer);
+                writeAction?.Invoke(ctx.Writer);
+            }, readFunc);
+
+        private bool SingleKeyOp(ClientOp op, T item)
+        {
+            IgniteArgumentCheck.NotNull(item, nameof(item));
+
+            return _socket.DoOutInOpAffinity(
+                op,
+                ctx =>
+                {
+                    var w = ctx.Writer;
+
+                    WriteIdentity(w);
+                    w.WriteBoolean(true); // ServerKeepBinary
+                    w.WriteObject(item);
+                },
+                ctx => ctx.Stream.ReadBool(),
+                _cacheId,
+                GetAffinityKey(item));
+        }
+
+        private bool MultiKeyOp(ClientOp op, IEnumerable<T> items, out int itemsCount)
+        {
+            IgniteArgumentCheck.NotNull(items, nameof(items));
+
+            using var enumerator = items.GetEnumerator();
+
+            if (!enumerator.MoveNext())
+            {
+                itemsCount = 0;
+                return false;
+            }
+
+            var first = enumerator.Current;
+            var affKey = GetAffinityKey(first);
+            var count = 1;
+
+            var res = _socket.DoOutInOpAffinity(
+                op,
+                ctx =>
+                {
+                    var w = ctx.Writer;
+
+                    WriteIdentity(w);
+                    w.WriteBoolean(true); // ServerKeepBinary.
+
+                    var countPos = w.Stream.Position;
+                    w.WriteInt(0);
+
+                    w.WriteObjectDetached(first);
+
+                    // ReSharper disable once AccessToDisposedClosure
+                    while (enumerator.MoveNext())
+                    {
+                        // ReSharper disable once AccessToDisposedClosure
+                        w.WriteObjectDetached(enumerator.Current);
+                        count++;
+                    }
+
+                    var endPos = w.Stream.Position;
+                    w.Stream.Seek(countPos, SeekOrigin.Begin);
+                    w.Stream.WriteInt(count);
+                    w.Stream.Seek(endPos, SeekOrigin.Begin);
+                },
+                ctx => ctx.Reader.ReadBoolean(),
+                _cacheId,
+                affKey);
+
+            itemsCount = count;
+            return res;
+        }
+
+        private void WriteIdentity(BinaryWriter w)
+        {
+            w.WriteString(Name);
+            w.WriteInt(_cacheId);
+            w.WriteBoolean(Colocated);
+        }
+
+        private object GetAffinityKey(T key)
+        {
+            // See ClientIgniteSetImpl#affinityKey in Java for details.
+            return Colocated ? _nameHash : key;
+        }
+
+        private void WriteIteratorStart(ClientRequestContext ctx)
+        {
+            WriteIdentity(ctx.Writer);
+            ctx.Writer.WriteInt(_pageSize);
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/DataStructures/IgniteSetClientEnumerator.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/DataStructures/IgniteSetClientEnumerator.cs
new file mode 100644
index 00000000000..51de66e4574
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/DataStructures/IgniteSetClientEnumerator.cs
@@ -0,0 +1,151 @@
+/*
+ * 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.DataStructures
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using Apache.Ignite.Core.Binary;
+
+    /// <summary>
+    /// Enumerator for <see cref="IgniteSetClient{T}"/>.
+    /// </summary>
+    internal sealed class IgniteSetClientEnumerator<T> : IEnumerator<T>
+    {
+        /** */
+        private readonly ClientSocket _socket;
+
+        /** */
+        private readonly int _pageSize;
+
+        /** */
+        private long? _resourceId;
+
+        /** */
+        private List<T> _page;
+
+        /** */
+        private int _pos = -1;
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="IgniteSetClientEnumerator{T}"/> class.
+        /// </summary>
+        /// <param name="ctx">Response context.</param>
+        /// <param name="pageSize">Page size.</param>
+        public IgniteSetClientEnumerator(ClientResponseContext ctx, int pageSize)
+        {
+            _page = ReadPage(ctx.Reader);
+
+            var hasNext = ctx.Reader.ReadBoolean();
+
+            _resourceId = hasNext ? ctx.Reader.ReadLong() : (long?)null;
+            _socket = ctx.Socket;
+            _pageSize = pageSize;
+        }
+
+        /** <inheritdoc /> */
+        public bool MoveNext()
+        {
+            if (_pos < _page.Count - 1)
+            {
+                _pos++;
+                return true;
+            }
+
+            if (_resourceId == null)
+            {
+                return false;
+            }
+
+            _socket.DoOutInOp<object>(ClientOp.SetIteratorGetPage,
+                ctx =>
+                {
+                    ctx.Stream.WriteLong(_resourceId.Value);
+                    ctx.Stream.WriteInt(_pageSize);
+                },
+                ctx =>
+                {
+                    _page = ReadPage(ctx.Reader);
+
+                    var hasNext = ctx.Reader.ReadBoolean();
+
+                    if (!hasNext)
+                    {
+                        _resourceId = null;
+                    }
+
+                    return null;
+                });
+
+            _pos = 0;
+            return true;
+        }
+
+        /** <inheritdoc /> */
+        public void Reset() => throw new NotSupportedException();
+
+        /** <inheritdoc /> */
+        public T Current
+        {
+            get
+            {
+                if (_pos == -1)
+                {
+                    throw new InvalidOperationException("Enumeration has not started. Call MoveNext.");
+                }
+
+                if (_pos >= _page.Count)
+                {
+                    throw new InvalidOperationException("Enumeration already finished.");
+                }
+
+                return _page[_pos];
+            }
+        }
+
+        /** <inheritdoc /> */
+        object IEnumerator.Current => Current;
+
+        /** <inheritdoc /> */
+        public void Dispose()
+        {
+            if (_resourceId == null)
+            {
+                return;
+            }
+
+            _socket.DoOutInOp<object>(ClientOp.ResourceClose, ctx => ctx.Stream.WriteLong(_resourceId.Value), null);
+
+            _resourceId = null;
+            _pos = int.MaxValue;
+        }
+
+        private static List<T> ReadPage(IBinaryRawReader r)
+        {
+            var size = r.ReadInt();
+            var res = new List<T>(size);
+
+            for (int i = 0; i < size; i++)
+            {
+                res.Add(r.ReadObject<T>());
+            }
+
+            return res;
+        }
+    }
+}
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 728458c7d40..635d07f355e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/IgniteClient.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/IgniteClient.cs
@@ -359,6 +359,45 @@ namespace Apache.Ignite.Core.Impl.Client
             return res;
         }
 
+        /** <inheritDoc /> */
+        public IIgniteSetClient<T> GetIgniteSet<T>(string name, CollectionClientConfiguration configuration)
+        {
+            IgniteArgumentCheck.NotNullOrEmpty(name, "name");
+
+            return _socket.DoOutInOp(ClientOp.SetGetOrCreate, ctx =>
+            {
+                var w = ctx.Writer;
+
+                w.WriteString(name);
+
+                if (configuration != null)
+                {
+                    w.WriteBoolean(true);
+                    w.WriteByte((byte)configuration.AtomicityMode);
+                    w.WriteByte((byte)configuration.CacheMode);
+                    w.WriteInt(configuration.Backups);
+                    w.WriteString(configuration.GroupName);
+                    w.WriteBoolean(configuration.Colocated);
+                }
+                else
+                {
+                    w.WriteBoolean(false);
+                }
+
+            }, ctx =>
+            {
+                if (!ctx.Reader.ReadBoolean())
+                {
+                    return null;
+                }
+
+                var colocated = ctx.Reader.ReadBoolean();
+                var cacheId = ctx.Reader.ReadInt();
+
+                return new IgniteSetClient<T>(_socket, name, colocated, cacheId);
+            });
+        }
+
         /** <inheritDoc /> */
         public IBinaryProcessor BinaryProcessor
         {