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 fcc76f6  IGNITE-2890 .NET: Add CacheConfiguration.NodeFilter
fcc76f6 is described below

commit fcc76f65835b41cd952b5bfe23a897977e94b6f7
Author: Pavel Tupitsyn <ptupit...@apache.org>
AuthorDate: Tue Nov 3 18:31:01 2020 +0300

    IGNITE-2890 .NET: Add CacheConfiguration.NodeFilter
    
    * Add CacheConfiguration.NodeFilter
    * Add AttributeNodeFilter - the only allowed IClusterNodeFilter 
implementation for cache node filter, maps to the same class in Java
---
 .../platform/utils/PlatformConfigurationUtils.java |  49 +++-
 .../apache/ignite/util/AttributeNodeFilter.java    |  14 +-
 .../Apache.Ignite.Core.Tests.DotNetCore.csproj     |   1 +
 .../Apache.Ignite.Core.Tests.csproj                |   4 +
 .../Cache/CacheNodeFilterTest.cs                   | 274 +++++++++++++++++++++
 .../Config/cache-attribute-node-filter.xml         |  81 ++++++
 .../Config/full-config.xml                         |   7 +
 .../IgniteConfigurationSerializerTest.cs           |   9 +
 .../Apache.Ignite.Core/Apache.Ignite.Core.csproj   |   1 +
 .../Cache/Configuration/CacheConfiguration.cs      |  32 ++-
 .../Cluster/AttributeNodeFilter.cs                 | 129 ++++++++++
 .../IgniteConfigurationSection.xsd                 |  12 +
 12 files changed, 609 insertions(+), 4 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
index 9cbef12..750c351 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/utils/PlatformConfigurationUtils.java
@@ -111,6 +111,7 @@ import 
org.apache.ignite.spi.eventstorage.memory.MemoryEventStorageSpi;
 import org.apache.ignite.ssl.SslContextFactory;
 import org.apache.ignite.transactions.TransactionConcurrency;
 import org.apache.ignite.transactions.TransactionIsolation;
+import org.apache.ignite.util.AttributeNodeFilter;
 
 /**
  * Configuration utils.
@@ -246,6 +247,8 @@ public class PlatformConfigurationUtils {
         ccfg.setAffinity(readAffinityFunction(in));
         ccfg.setExpiryPolicyFactory(readExpiryPolicyFactory(in));
 
+        ccfg.setNodeFilter(readAttributeNodeFilter(in));
+
         int keyCnt = in.readInt();
 
         if (keyCnt > 0) {
@@ -347,6 +350,48 @@ public class PlatformConfigurationUtils {
     }
 
     /**
+     * Reads the node filter config.
+     *
+     * @param in Stream.
+     * @return AttributeNodeFilter.
+     */
+    public static AttributeNodeFilter readAttributeNodeFilter(BinaryRawReader 
in) {
+        if (!in.readBoolean())
+            return null;
+
+        int cnt = in.readInt();
+
+        Map<String, Object> attrs = new HashMap<>(cnt);
+        for (int i = 0; i < cnt; i++)
+            attrs.put(in.readString(), in.readObject());
+
+        return new AttributeNodeFilter(attrs);
+    }
+
+    /**
+     * Writes the node filter.
+     * @param out Stream.
+     * @param nodeFilter IgnitePredicate.
+     */
+    private static void writeAttributeNodeFilter(BinaryRawWriter out, 
IgnitePredicate nodeFilter) {
+        if (!(nodeFilter instanceof AttributeNodeFilter)) {
+            out.writeBoolean(false);
+            return;
+        }
+
+        out.writeBoolean(true);
+
+        Map<String, Object> attrs = ((AttributeNodeFilter) 
nodeFilter).getAttrs();
+
+        out.writeInt(attrs.size());
+
+        for (Map.Entry<String, Object> entry : attrs.entrySet()) {
+            out.writeString(entry.getKey());
+            out.writeObject(entry.getValue());
+        }
+    }
+
+    /**
      * Reads the eviction policy.
      *
      * @param in Stream.
@@ -417,7 +462,7 @@ public class PlatformConfigurationUtils {
     }
 
     /**
-     * Reads the near config.
+     * Writes the near config.
      *
      * @param out Stream.
      * @param cfg NearCacheConfiguration.
@@ -1084,6 +1129,8 @@ public class PlatformConfigurationUtils {
         writeAffinityFunction(writer, ccfg.getAffinity());
         writeExpiryPolicyFactory(writer, ccfg.getExpiryPolicyFactory());
 
+        writeAttributeNodeFilter(writer, ccfg.getNodeFilter());
+
         CacheKeyConfiguration[] keys = ccfg.getKeyConfiguration();
 
         if (keys != null) {
diff --git 
a/modules/core/src/main/java/org/apache/ignite/util/AttributeNodeFilter.java 
b/modules/core/src/main/java/org/apache/ignite/util/AttributeNodeFilter.java
index fed0d43..70c5a29 100644
--- a/modules/core/src/main/java/org/apache/ignite/util/AttributeNodeFilter.java
+++ b/modules/core/src/main/java/org/apache/ignite/util/AttributeNodeFilter.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.util;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import org.apache.ignite.cluster.ClusterGroup;
 import org.apache.ignite.cluster.ClusterNode;
@@ -42,7 +43,7 @@ import org.jetbrains.annotations.Nullable;
  * attribute set to value {@code data}:
  * <pre name="code" class="xml">
  * &lt;property name=&quot;nodeFilter&quot;&gt;
- *     &lt;bean 
class=&quot;org.apache.ignite.util.ClusterAttributeNodeFilter&quot;&gt;
+ *     &lt;bean 
class=&quot;org.apache.ignite.util.AttributeNodeFilter&quot;&gt;
  *         &lt;constructor-arg value="group"/&gt;
  *         &lt;constructor-arg value="data"/&gt;
  *     &lt;/bean&gt;
@@ -51,7 +52,7 @@ import org.jetbrains.annotations.Nullable;
  * You can also specify multiple attributes for the filter:
  * <pre name="code" class="xml">
  * &lt;property name=&quot;nodeFilter&quot;&gt;
- *     &lt;bean 
class=&quot;org.apache.ignite.util.ClusterAttributeNodeFilter&quot;&gt;
+ *     &lt;bean 
class=&quot;org.apache.ignite.util.AttributeNodeFilter&quot;&gt;
  *         &lt;constructor-arg&gt;
  *             &lt;map&gt;
  *                 &lt;entry key=&quot;cpu-group&quot; 
value=&quot;high&quot;/&gt;
@@ -105,4 +106,13 @@ public class AttributeNodeFilter implements 
IgnitePredicate<ClusterNode> {
 
         return true;
     }
+
+    /**
+     * Gets attributes.
+     *
+     * @return Attributes collection.
+     */
+    public Map<String, Object> getAttrs() {
+        return new HashMap<>(attrs);
+    }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.DotNetCore.csproj
 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.DotNetCore.csproj
index ef97e9a..884009a 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.DotNetCore.csproj
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.DotNetCore.csproj
@@ -89,6 +89,7 @@
     <None Update="Config\Cache\Store\cache-store-session-shared-factory.xml" 
CopyToOutputDirectory="PreserveNewest" />
     <None Update="Config\Cache\Affinity\affinity-function.xml" 
CopyToOutputDirectory="PreserveNewest" />
     <None Update="Config\Cache\Affinity\affinity-function2.xml" 
CopyToOutputDirectory="PreserveNewest" />
+    <None Update="Config\cache-attribute-node-filter.xml" 
CopyToOutputDirectory="PreserveNewest" />
     <None Update="custom_app.config">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
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 0f67ed4..b28268d 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
@@ -112,6 +112,7 @@
     <Compile Include="Binary\Serializable\DynamicFieldSetTest.cs" />
     <Compile Include="Cache\Affinity\AffinityAttributeTest.cs" />
     <Compile Include="Cache\CacheCreateTest.cs" />
+    <Compile Include="Cache\CacheNodeFilterTest.cs" />
     <Compile Include="Cache\CacheQueryMetricsTest.cs" />
     <Compile Include="Cache\CacheTimeoutOnPmeTransactionalTest.cs" />
     <Compile Include="Cache\CacheTransactionGridStopTest.cs" />
@@ -439,6 +440,9 @@
     </ProjectReference>
   </ItemGroup>
   <ItemGroup>
+    <Content Include="Config\cache-attribute-node-filter.xml">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="Config\cache-binarizables.xml">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheNodeFilterTest.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheNodeFilterTest.cs
new file mode 100644
index 0000000..2c94775
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/CacheNodeFilterTest.cs
@@ -0,0 +1,274 @@
+/*
+ * 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.Cache
+{
+    using System;
+    using System.Collections.Generic;
+    using System.IO;
+    using System.Linq;
+    using Apache.Ignite.Core.Cache.Configuration;
+    using Apache.Ignite.Core.Cluster;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Cache node filter tests.
+    /// </summary>
+    [TestFixture]
+    public class CacheNodeFilterTest
+    {
+        /** */
+        private const string AttrKey2 = "attr2";
+
+        /**  */
+        private const int AttrVal2 = 3;
+
+        /** */
+        private const string AttrKey3 = "my-key";
+
+        /**  */
+        private const string AttrVal3 = "my-val";
+
+        /** Grid instances. */
+        private IIgnite _grid1, _grid2, _grid3;
+
+        /// <summary>
+        ///  Fixture setup.
+        /// </summary>
+        [TestFixtureSetUp]
+        public void TestFixtureSetUp()
+        {
+            var springConfig = new 
IgniteConfiguration(TestUtils.GetTestConfiguration())
+            {
+                SpringConfigUrl = Path.Combine("Config", 
"cache-attribute-node-filter.xml"),
+                IgniteInstanceName = "springGrid"
+            };
+            _grid1 = Ignition.Start(springConfig);
+
+            _grid2 = Ignition.Start(GetTestConfiguration("Ignite2",
+                new Dictionary<string, object>
+                {
+                    {AttrKey2, AttrVal2}
+                }));
+
+            _grid3 = Ignition.Start(GetTestConfiguration("Ignite3",
+                new Dictionary<string, object>
+                {
+                    {AttrKey2, AttrVal2},
+                    {AttrKey3, AttrVal3}
+                }));
+        }
+
+        /// <summary>
+        ///  Fixture tear down.
+        /// </summary>
+        [TestFixtureTearDown]
+        public void TestFixtureTearDown()
+        {
+            Ignition.StopAll(true);
+        }
+
+        /// <summary>
+        /// Gets a test configuration.
+        /// </summary>
+        /// <param name="gridName">Grid name.</param>
+        /// <param name="userAttributes">User attributes.</param>
+        /// <returns></returns>
+        private IgniteConfiguration GetTestConfiguration(string gridName, 
Dictionary<string, object> userAttributes)
+        {
+            IgniteConfiguration cfg = TestUtils.GetTestConfiguration(name: 
gridName);
+            cfg.UserAttributes = userAttributes;
+            return cfg;
+        }
+
+        /// <summary>
+        /// Tests attribute node filter with a custom user attribute name
+        /// and null value always matches.
+        /// </summary>
+        [Test]
+        public void TestUserAttributeWithNullValueMatchesAllNodes()
+        {
+            const int replicatedPartitionsCount = 512;
+
+            var cacheCfg = new CacheConfiguration
+            {
+                Name = Guid.NewGuid().ToString(),
+                NodeFilter = new AttributeNodeFilter("my.custom.attr", null),
+                CacheMode = CacheMode.Replicated,
+            };
+            var cache = _grid1.CreateCache<object, object>(cacheCfg);
+
+            var affinity = _grid1.GetAffinity(cache.Name);
+
+            Assert.AreEqual(3, 
_grid1.GetCluster().ForDataNodes(cache.Name).GetNodes().Count);
+
+            var parts1 = 
affinity.GetAllPartitions(_grid1.GetCluster().GetLocalNode());
+            var parts2 = 
affinity.GetAllPartitions(_grid2.GetCluster().GetLocalNode());
+            var parts3 = 
affinity.GetAllPartitions(_grid3.GetCluster().GetLocalNode());
+
+            Assert.AreEqual(replicatedPartitionsCount, parts1.Length);
+            Assert.AreEqual(parts1, parts2);
+            Assert.AreEqual(parts2, parts3);
+        }
+
+        /// <summary>
+        /// Tests attribute node filter matches the specified attribute.
+        /// </summary>
+        [Test]
+        public void TestAttributeNodeFilterMatchesCustomNode()
+        {
+            const int itemsCount = 10;
+
+            var cacheCfg = new CacheConfiguration
+            {
+                Name = Guid.NewGuid().ToString(),
+                NodeFilter = new AttributeNodeFilter(AttrKey2, AttrVal2),
+                CacheMode = CacheMode.Replicated,
+            };
+            var cache = _grid1.CreateCache<object, object>(cacheCfg);
+
+            for (int i = 0; i < itemsCount; i++)
+            {
+                cache.Put(i, i);
+            }
+
+            Assert.AreEqual(2, 
_grid1.GetCluster().ForDataNodes(cache.Name).GetNodes().Count);
+
+            Assert.AreEqual(0, cache.GetLocalEntries().Count());
+
+            var cache2 = _grid2.GetCache<object, object>(cache.Name);
+            var cache3 = _grid2.GetCache<object, object>(cache.Name);
+
+            Assert.AreEqual(itemsCount, cache2.GetLocalEntries().Count());
+            Assert.AreEqual(itemsCount, cache3.GetLocalEntries().Count());
+        }
+
+        /// <summary>
+        /// Tests node filter with multiple attributes matches single node.
+        /// </summary>
+        [Test]
+        public void TestNodeFilterWithMultipleUserAttributes()
+        {
+            var cacheCfg = new CacheConfiguration
+            {
+                Name = Guid.NewGuid().ToString(),
+                NodeFilter = new AttributeNodeFilter
+                {
+                    Attributes = new Dictionary<string, object>
+                    {
+                        {AttrKey2, AttrVal2},
+                        {AttrKey3, AttrVal3}
+                    }
+                },
+                CacheMode = CacheMode.Replicated,
+            };
+            var cache = _grid1.CreateCache<object, object>(cacheCfg);
+
+            ICollection<IClusterNode> dataNodes = 
_grid1.GetCluster().ForDataNodes(cache.Name).GetNodes();
+            Assert.AreEqual(1, dataNodes.Count);
+            Assert.AreEqual(_grid3.GetCluster().GetLocalNode(), 
dataNodes.Single());
+        }
+
+        /// <summary>
+        /// Tests Java and .NET nodes can utilize the same
+        /// attribute node filter configuration.
+        /// </summary>
+        [Test]
+        public void TestSpringAttributeNodeFilter()
+        {
+            var cache = _grid1.GetCache<object, object>("cache");
+            Assert.AreEqual(2, 
_grid1.GetCluster().ForDataNodes(cache.Name).GetNodes().Count);
+
+            var nodeFilter = cache.GetConfiguration().NodeFilter as 
AttributeNodeFilter;
+            Assert.IsNotNull(nodeFilter);
+
+            Assert.AreEqual(1, nodeFilter.Attributes.Count);
+
+            var expected = new KeyValuePair<string, object>(AttrKey3, 
AttrVal3);
+            Assert.AreEqual(expected, nodeFilter.Attributes.Single());
+        }
+
+        /// <summary>
+        /// Tests that java node filter is not being read on .NET side.
+        /// </summary>
+        [Test]
+        public void TestJavaNodeFilterIsNotAccessedByNetConfig()
+        {
+            var cache = _grid1.GetCache<object, object>("cacheWithJavaFilter");
+
+            Assert.IsNull(cache.GetConfiguration().NodeFilter);
+        }
+
+        /// <summary>
+        /// Tests that custom node filter is not supported.
+        /// </summary>
+        [Test]
+        public void TestCustomFilterIsNotSupported()
+        {
+            var cacheCfg = new CacheConfiguration
+            {
+                Name = Guid.NewGuid().ToString(),
+                CacheMode = CacheMode.Replicated,
+                NodeFilter = new CustomFilter()
+            };
+
+            TestDelegate action = () => { _grid1.CreateCache<object, 
object>(cacheCfg); };
+
+            var ex = Assert.Throws<NotSupportedException>(action);
+            Assert.AreEqual("Unsupported CacheConfiguration.NodeFilter: " +
+                            "'CustomFilter'. " +
+                            "Only predefined implementations are supported: " +
+                            "'AttributeNodeFilter'", ex.Message);
+        }
+
+        /// <summary>
+        /// Tests that attribute node filter with <code>Null</code>
+        /// Attributes value is not supported.
+        /// </summary>
+        [Test]
+        public void TestAttributeFilterWithNullValues()
+        {
+            TestDelegate action = () =>
+            {
+                var _ = new CacheConfiguration
+                {
+                    NodeFilter = new AttributeNodeFilter
+                    {
+                        Attributes = null
+                    },
+                };
+            };
+
+            var ex = Assert.Throws<ArgumentNullException>(action);
+            StringAssert.Contains("value", ex.Message);
+        }
+
+        /// <summary>
+        /// Custom node filter.
+        /// </summary>
+        public class CustomFilter : IClusterNodeFilter
+        {
+            /// <summary>
+            /// <inheritdoc cref="IClusterNodeFilter.Invoke"/>
+            /// </summary>
+            public bool Invoke(IClusterNode node)
+            {
+                return true;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/cache-attribute-node-filter.xml
 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/cache-attribute-node-filter.xml
new file mode 100644
index 0000000..8b1c543
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/cache-attribute-node-filter.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns:util="http://www.springframework.org/schema/util";
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd";>
+  <bean id="grid.cfg" 
class="org.apache.ignite.configuration.IgniteConfiguration">
+    <property name="localHost" value="127.0.0.1"/>
+    <property name="connectorConfiguration">
+      <null/>
+    </property>
+
+    <property name="userAttributes">
+      <map>
+        <entry key="my-key" value="my-val"/>
+      </map>
+    </property>
+
+
+    <property name="cacheConfiguration">
+      <list>
+        <bean class="org.apache.ignite.configuration.CacheConfiguration">
+          <property name="name" value="cache"/>
+          <property name="nodeFilter">
+            <bean class="org.apache.ignite.util.AttributeNodeFilter">
+              <constructor-arg>
+                <map>
+                  <entry key="my-key" value="my-val"/>
+                </map>
+              </constructor-arg>
+            </bean>
+          </property>
+        </bean>
+        <bean class="org.apache.ignite.configuration.CacheConfiguration">
+          <property name="name" value="cacheWithJavaFilter"/>
+          <property name="nodeFilter">
+            <bean 
class="org.apache.ignite.platform.PlatformAttributeNodeFilter">
+            </bean>
+          </property>
+        </bean>
+      </list>
+    </property>
+
+    <property name="discoverySpi">
+      <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+        <property name="ipFinder">
+          <bean 
class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">
+            <property name="addresses">
+              <list>
+                <!-- In distributed environment, replace with actual host IP 
address. -->
+                <value>127.0.0.1:47500</value>
+              </list>
+            </property>
+          </bean>
+        </property>
+        <property name="socketTimeout" value="300" />
+      </bean>
+    </property>
+  </bean>
+</beans>
\ No newline at end of file
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml
index e9e5ff4..32eed22 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Config/full-config.xml
@@ -91,6 +91,13 @@
             <keyConfiguration>
                 <cacheKeyConfiguration typeName='foo' 
affinityKeyFieldName='bar' />
             </keyConfiguration>
+            <nodeFilter type='Apache.Ignite.Core.Cluster.AttributeNodeFilter'>
+              <attributes>
+                <pair key='myNode' value='true' />
+                <pair key='foo' />
+                <pair key='baz'>null</pair>
+              </attributes>
+            </nodeFilter>
         </cacheConfiguration>
         <cacheConfiguration name='secondCache' />
     </cacheConfiguration>
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
index 7941175..1211d38 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationSerializerTest.cs
@@ -39,6 +39,7 @@ namespace Apache.Ignite.Core.Tests
     using Apache.Ignite.Core.Cache.Eviction;
     using Apache.Ignite.Core.Cache.Expiry;
     using Apache.Ignite.Core.Cache.Store;
+    using Apache.Ignite.Core.Cluster;
     using Apache.Ignite.Core.Ssl;
     using Apache.Ignite.Core.Common;
     using Apache.Ignite.Core.Communication.Tcp;
@@ -159,6 +160,14 @@ namespace Apache.Ignite.Core.Tests
             Assert.IsNotNull(nearCfg);
             Assert.AreEqual(7, nearCfg.NearStartSize);
 
+            var nodeFilter = (AttributeNodeFilter)cacheCfg.NodeFilter;
+            Assert.IsNotNull(nodeFilter);
+            var attributes = nodeFilter.Attributes.ToList();
+            Assert.AreEqual(3, nodeFilter.Attributes.Count);
+            Assert.AreEqual(new KeyValuePair<string, object>("myNode", 
"true"), attributes[0]);
+            Assert.AreEqual(new KeyValuePair<string, object>("foo", null), 
attributes[1]);
+            Assert.AreEqual(new KeyValuePair<string, object>("baz", null), 
attributes[2]);
+
             var plc = nearCfg.EvictionPolicy as FifoEvictionPolicy;
             Assert.IsNotNull(plc);
             Assert.AreEqual(10, plc.BatchSize);
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 99bf6fd..50e3db7 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
@@ -71,6 +71,7 @@
     <Compile Include="Client\ISslStreamFactory.cs" />
     <Compile Include="Client\Services\IServicesClient.cs" />
     <Compile Include="Client\SslStreamFactory.cs" />
+    <Compile Include="Cluster\AttributeNodeFilter.cs" />
     <Compile Include="Client\Transactions\ITransactionClient.cs" />
     <Compile Include="Client\Transactions\ITransactionsClient.cs" />
     <Compile Include="Client\Transactions\TransactionClientConfiguration.cs" />
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/CacheConfiguration.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/CacheConfiguration.cs
index 82eb16c..56681cf 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/CacheConfiguration.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/CacheConfiguration.cs
@@ -34,6 +34,7 @@ namespace Apache.Ignite.Core.Cache.Configuration
     using Apache.Ignite.Core.Cache.Eviction;
     using Apache.Ignite.Core.Cache.Expiry;
     using Apache.Ignite.Core.Cache.Store;
+    using Apache.Ignite.Core.Cluster;
     using Apache.Ignite.Core.Common;
     using Apache.Ignite.Core.Configuration;
     using Apache.Ignite.Core.Impl;
@@ -249,7 +250,7 @@ namespace Apache.Ignite.Core.Cache.Configuration
         /// Initializes a new instance of the <see cref="CacheConfiguration"/> 
class,
         /// performing a deep copy of specified cache configuration.
         /// </summary>
-        /// <param name="other">The other configuration to perfrom deep copy 
from.</param>
+        /// <param name="other">The other configuration to perform deep copy 
from.</param>
         public CacheConfiguration(CacheConfiguration other)
         {
             if (other != null)
@@ -340,6 +341,8 @@ namespace Apache.Ignite.Core.Cache.Configuration
             AffinityFunction = AffinityFunctionSerializer.Read(reader);
             ExpiryPolicyFactory = 
ExpiryPolicySerializer.ReadPolicyFactory(reader);
 
+            NodeFilter = reader.ReadBoolean() ? new 
AttributeNodeFilter(reader) : null;
+
             KeyConfiguration = reader.ReadCollectionRaw(r => new 
CacheKeyConfiguration(r));
             
             if (reader.ReadBoolean())
@@ -448,6 +451,26 @@ namespace Apache.Ignite.Core.Cache.Configuration
             AffinityFunctionSerializer.Write(writer, AffinityFunction);
             ExpiryPolicySerializer.WritePolicyFactory(writer, 
ExpiryPolicyFactory);
 
+            if (NodeFilter != null)
+            {
+                writer.WriteBoolean(true);
+
+                var attributeNodeFilter = NodeFilter as AttributeNodeFilter;
+                if (attributeNodeFilter == null)
+                {
+                    throw new NotSupportedException(string.Format(
+                        "Unsupported CacheConfiguration.NodeFilter: '{0}'. " +
+                        "Only predefined implementations are supported: '{1}'",
+                        NodeFilter.GetType().Name, 
typeof(AttributeNodeFilter).Name));
+                }
+
+                attributeNodeFilter.Write(writer);
+            }
+            else
+            {
+                writer.WriteBoolean(false);
+            }
+
             writer.WriteCollectionRaw(KeyConfiguration);
             
             if (PlatformCacheConfiguration != null)
@@ -949,5 +972,12 @@ namespace Apache.Ignite.Core.Cache.Configuration
         /// </summary>
         [IgniteExperimental]
         public PlatformCacheConfiguration PlatformCacheConfiguration { get; 
set; }
+
+        /// <summary>
+        /// Gets or sets the cluster node filter. Cache will be started only 
on nodes that match the filter.
+        /// <para />
+        /// Only predefined implementations are supported: <see 
cref="AttributeNodeFilter"/>.
+        /// </summary>
+        public IClusterNodeFilter NodeFilter { get; set; }
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core/Cluster/AttributeNodeFilter.cs 
b/modules/platforms/dotnet/Apache.Ignite.Core/Cluster/AttributeNodeFilter.cs
new file mode 100644
index 0000000..4c01c0e
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Cluster/AttributeNodeFilter.cs
@@ -0,0 +1,129 @@
+/*
+ * 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.Cluster
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Diagnostics;
+    using System.Diagnostics.CodeAnalysis;
+    using Apache.Ignite.Core.Binary;
+    using Apache.Ignite.Core.Impl.Common;
+
+    /// <summary>
+    /// Attribute node filter.
+    /// <para />
+    /// The filter will evaluate to true if a node has all specified 
attributes with corresponding values.
+    /// <para />
+    /// You can set node attributes using <see 
cref="IgniteConfiguration.UserAttributes"/> property. 
+    /// </summary>
+    public sealed class AttributeNodeFilter : IClusterNodeFilter
+    {
+        /** */
+        private IDictionary<string, object> _attributes;
+
+        /// <summary>
+        /// Attributes dictionary match.
+        /// </summary>
+        [SuppressMessage("Microsoft.Usage", 
"CA2227:CollectionPropertiesShouldBeReadOnly")]
+        public IDictionary<string, object> Attributes
+        {
+            get { return _attributes; }
+            set
+            {
+                if (value == null)
+                {
+                    throw new ArgumentNullException("value");
+                }
+
+                _attributes = value;
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="AttributeNodeFilter"/>.
+        /// </summary>
+        public AttributeNodeFilter()
+        {
+            // No-op.
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="AttributeNodeFilter"/>.
+        /// </summary>
+        /// <param name="attrName">Attribute name.</param>
+        /// <param name="attrValue">Attribute value.</param>
+        public AttributeNodeFilter(string attrName, object attrValue)
+        {
+            IgniteArgumentCheck.NotNullOrEmpty(attrName, "attrName");
+
+            Attributes = new Dictionary<string, object>(1)
+            {
+                {attrName, attrValue}
+            };
+        }
+
+        /** <inheritdoc /> */
+        public bool Invoke(IClusterNode node)
+        {
+            throw new NotSupportedException("Should not be called from .NET 
side.");
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="AttributeNodeFilter"/> 
from a binary reader.
+        /// </summary>
+        /// <param name="reader">Reader.</param>
+        internal AttributeNodeFilter(IBinaryRawReader reader)
+        {
+            IgniteArgumentCheck.NotNull(reader, "reader");
+
+            int count = reader.ReadInt();
+
+            Debug.Assert(count > 0);
+
+            Attributes = new Dictionary<string, object>(count);
+
+            while (count > 0)
+            {
+                string attrKey = reader.ReadString();
+                object attrVal = reader.ReadObject<object>();
+
+                Debug.Assert(attrKey != null);
+
+                Attributes[attrKey] = attrVal;
+
+                count--;
+            }
+        }
+
+        /// <summary>
+        /// Writes the instance to a writer.
+        /// </summary>
+        /// <param name="writer">Writer.</param>
+        internal void Write(IBinaryRawWriter writer)
+        {
+            writer.WriteInt(Attributes.Count);
+
+            // Does not preserve ordering, it's fine.
+            foreach (KeyValuePair<string, object> attr in Attributes)
+            {
+                writer.WriteString(attr.Key);
+                writer.WriteObject(attr.Value);
+            }
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd 
b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd
index 0ab59ec..314b839 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteConfigurationSection.xsd
@@ -623,6 +623,18 @@
                                                 </xs:attribute>
                                             </xs:complexType>
                                         </xs:element>
+                                        <xs:element name="nodeFilter" 
minOccurs="0" maxOccurs="1">
+                                          <xs:annotation>
+                                            <xs:documentation>Node filter to 
match selected nodes. Only predefined AttributeNodeFilter is 
supported.</xs:documentation>
+                                          </xs:annotation>
+                                          <xs:complexType>
+                                            <xs:attribute name="type" 
type="xs:string" use="required">
+                                              <xs:annotation>
+                                                
<xs:documentation>Assembly-qualified type name.</xs:documentation>
+                                              </xs:annotation>
+                                            </xs:attribute>
+                                          </xs:complexType>
+                                        </xs:element>
                                         <xs:element name="keyConfiguration" 
minOccurs="0">
                                             <xs:annotation>
                                                 <xs:documentation>Cache key 
configuration collection.</xs:documentation>

Reply via email to