ONOS-7898 Action profile group/member refactoring

Also includes:
- New abstract P4Runtime codec implementation. Currently used for action
profile members/groups encoding/deconding, the plan is to handle all
other codecs via this.
- Improved read requests in P4RuntimeClientImpl
- Removed handling of max group size in P4Runtime driver. Instead, added
modified group translator to specify a max group size by using
information from the pipeline model.

Change-Id: I684bae0184d683bb448ba19863c561f9848479d2
(cherry picked from commit 99c59dbb0351f5389b822bfe2628faf0ff5ab22e)
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiActionProfileModel.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiActionProfileModel.java
index 66b8c53..0a66ee9 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiActionProfileModel.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiActionProfileModel.java
@@ -41,16 +41,28 @@
     Collection<PiTableId> tables();
 
     /**
-     * Returns true if this action profile implements dynamic selection, false otherwise.
+     * Returns true if this action profile implements dynamic selection, false
+     * otherwise.
      *
-     * @return true if action profile implements dynamic selection, false otherwise
+     * @return true if action profile implements dynamic selection, false
+     * otherwise
      */
     boolean hasSelector();
 
     /**
-     * Returns the maximum number of member entries of this action profile.
+     * Returns the maximum number of member entries that this action profile can
+     * hold.
      *
      * @return maximum number of member entries
      */
-    long maxSize();
+    long size();
+
+    /**
+     * Returns the maximum number of members a group of this action profile can
+     * hold. This method is meaningful only if the action profile implements
+     * dynamic selection. 0 signifies that an explicit limit is not set.
+     *
+     * @return maximum number of members a group can hold
+     */
+    int maxGroupSize();
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiAction.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiAction.java
index 24655b6..78a62fd 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiAction.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiAction.java
@@ -30,21 +30,24 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Instance of an action, and its runtime parameters, of a table entry in a protocol-independent pipeline.
+ * Instance of an action, and its runtime parameters, of a table entry in a
+ * protocol-independent pipeline.
  */
 @Beta
 public final class PiAction implements PiTableAction {
 
     private final PiActionId actionId;
-    private final Map<PiActionParamId, PiActionParam> runtimeParams;
+    private final ImmutableMap<PiActionParamId, PiActionParam> runtimeParams;
 
     /**
-     * Creates a new action instance for the given action identifier and runtime parameters.
+     * Creates a new action instance for the given action identifier and runtime
+     * parameters.
      *
      * @param actionId      action identifier
      * @param runtimeParams list of runtime parameters
      */
-    private PiAction(PiActionId actionId, Map<PiActionParamId, PiActionParam> runtimeParams) {
+    private PiAction(PiActionId actionId,
+                     Map<PiActionParamId, PiActionParam> runtimeParams) {
         this.actionId = actionId;
         this.runtimeParams = ImmutableMap.copyOf(runtimeParams);
     }
@@ -64,8 +67,8 @@
     }
 
     /**
-     * Returns all runtime parameters of this action. Return an empty collection if the action doesn't take any runtime
-     * parameters.
+     * Returns all runtime parameters of this action. Return an empty collection
+     * if the action doesn't take any runtime parameters.
      *
      * @return list of byte sequences
      */
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java
index 96168b9..f86e70c 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroup.java
@@ -19,13 +19,15 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import org.onosproject.net.pi.model.PiActionProfileId;
 
 import java.util.Collection;
 import java.util.Map;
+import java.util.Optional;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
@@ -34,70 +36,96 @@
 @Beta
 public final class PiActionProfileGroup implements PiEntity {
 
-    private final PiActionProfileGroupId id;
-    private final ImmutableSet<PiActionProfileMember> members;
     private final PiActionProfileId actionProfileId;
+    private final PiActionProfileGroupId groupId;
+    private final ImmutableMap<PiActionProfileMemberId, WeightedMember> members;
+    private final int maxSize;
 
-    private PiActionProfileGroup(PiActionProfileGroupId id,
-                                 ImmutableSet<PiActionProfileMember> members,
-                                 PiActionProfileId actionProfileId) {
-        this.id = id;
+    private PiActionProfileGroup(PiActionProfileGroupId groupId,
+                                 ImmutableMap<PiActionProfileMemberId, WeightedMember> members,
+                                 PiActionProfileId actionProfileId,
+                                 int maxSize) {
+        this.groupId = groupId;
         this.members = members;
         this.actionProfileId = actionProfileId;
+        this.maxSize = maxSize;
     }
 
     /**
-     * Returns the identifier of this action profile group.
+     * Returns the ID of this action profile group.
      *
-     * @return action profile group identifier
+     * @return action profile group ID
      */
     public PiActionProfileGroupId id() {
-        return id;
+        return groupId;
     }
 
     /**
-     * Returns the members of this action profile group.
+     * Returns the list of member references of this action profile group.
      *
      * @return collection of action profile members.
      */
-    public Collection<PiActionProfileMember> members() {
-        return members;
+    public Collection<WeightedMember> members() {
+        return members.values();
     }
 
     /**
-     * Gets identifier of the action profile.
+     * Returns the group member identified by the given action profile member
+     * ID, if present.
      *
-     * @return action profile id
+     * @param memberId action profile member ID
+     * @return optional group member
      */
-    public PiActionProfileId actionProfileId() {
+    public Optional<WeightedMember> member(PiActionProfileMemberId memberId) {
+        return Optional.of(members.get(memberId));
+    }
+
+    /**
+     * Returns the maximum number of members that this group can hold. 0
+     * signifies that a limit is not set.
+     *
+     * @return maximum number of members that this group can hold
+     */
+    public int maxSize() {
+        return maxSize;
+    }
+
+    /**
+     * Returns the ID of the action profile where this group belong.
+     *
+     * @return action profile ID
+     */
+    public PiActionProfileId actionProfile() {
         return actionProfileId;
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) {
+    public boolean equals(Object obj) {
+        if (this == obj) {
             return true;
         }
-        if (o == null || !(o instanceof PiActionProfileGroup)) {
+        if (obj == null || getClass() != obj.getClass()) {
             return false;
         }
-        PiActionProfileGroup that = (PiActionProfileGroup) o;
-        return Objects.equal(id, that.id) &&
-                Objects.equal(members, that.members) &&
-                Objects.equal(actionProfileId, that.actionProfileId);
+        final PiActionProfileGroup other = (PiActionProfileGroup) obj;
+        return Objects.equal(this.groupId, other.groupId)
+                && Objects.equal(this.members, other.members)
+                && Objects.equal(this.maxSize, other.maxSize)
+                && Objects.equal(this.actionProfileId, other.actionProfileId);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(id, members);
+        return Objects.hashCode(groupId, members, maxSize, actionProfileId);
     }
 
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
-                .add("groupId", id)
+                .add("actionProfile", actionProfileId)
+                .add("id", groupId)
                 .add("members", members)
-                .add("piActionProfileId", actionProfileId)
+                .add("maxSize", maxSize)
                 .toString();
     }
 
@@ -120,55 +148,115 @@
      */
     public static final class Builder {
 
-        private PiActionProfileGroupId id;
-        private Map<PiActionProfileMemberId, PiActionProfileMember> members = Maps.newHashMap();
-        private PiActionProfileId piActionProfileId;
+        private PiActionProfileGroupId groupId;
+        private Map<PiActionProfileMemberId, WeightedMember> members = Maps.newHashMap();
+        private PiActionProfileId actionProfileId;
+        private int maxSize;
 
         private Builder() {
             // hides constructor.
         }
 
         /**
-         * Sets the identifier of this action profile group.
+         * Sets the ID of this action profile group.
          *
-         * @param id action profile group identifier
+         * @param id action profile group ID
          * @return this
          */
         public Builder withId(PiActionProfileGroupId id) {
-            this.id = id;
+            this.groupId = id;
             return this;
         }
 
         /**
-         * Adds one member to this action profile group.
+         * Adds one member to this action profile.
          *
-         * @param member action profile member
+         * @param member member to add
          * @return this
          */
-        public Builder addMember(PiActionProfileMember member) {
+        public Builder addMember(WeightedMember member) {
+            checkNotNull(member);
             members.put(member.id(), member);
             return this;
         }
 
         /**
-         * Adds many members to this action profile group.
+         * Adds one member to this action profile group with default weight.
          *
-         * @param members action profile members
+         * @param memberId ID of the action profile member to add
          * @return this
          */
-        public Builder addMembers(Collection<PiActionProfileMember> members) {
-            members.forEach(this::addMember);
+        public Builder addMember(PiActionProfileMemberId memberId) {
+            addMember(new WeightedMember(memberId, WeightedMember.DEFAULT_WEIGHT));
             return this;
         }
 
         /**
-         * Sets the identifier of the action profile.
+         * Adds one member to this action profile group with default weight.
          *
-         * @param piActionProfileId the identifier of the action profile
+         * @param memberInstance the action profile member instance to add
+         * @return this
+         */
+        public Builder addMember(PiActionProfileMember memberInstance) {
+            addMember(new WeightedMember(memberInstance, WeightedMember.DEFAULT_WEIGHT));
+            return this;
+        }
+
+        /**
+         * Adds all members to this action profile group with default weight.
+         *
+         * @param memberInstances the action profile member instance to add
+         * @return this
+         */
+        public Builder addMembers(Iterable<PiActionProfileMember> memberInstances) {
+            memberInstances.forEach(this::addMember);
+            return this;
+        }
+
+        /**
+         * Adds one member to this action profile group with the given weight.
+         *
+         * @param memberId ID of the action profile member to add
+         * @param weight   weight
+         * @return this
+         */
+        public Builder addMember(PiActionProfileMemberId memberId, int weight) {
+            addMember(new WeightedMember(memberId, weight));
+            return this;
+        }
+
+        /**
+         * Adds one member to this action profile group with the given weight.
+         *
+         * @param memberInstance the action profile member instance to add
+         * @param weight         weight
+         * @return this
+         */
+        public Builder addMember(PiActionProfileMember memberInstance, int weight) {
+            addMember(new WeightedMember(memberInstance, weight));
+            return this;
+        }
+
+        /**
+         * Sets the ID of the action profile.
+         *
+         * @param piActionProfileId the ID of the action profile
          * @return this
          */
         public Builder withActionProfileId(PiActionProfileId piActionProfileId) {
-            this.piActionProfileId = piActionProfileId;
+            this.actionProfileId = piActionProfileId;
+            return this;
+        }
+
+        /**
+         * Sets the maximum number of members that this group can hold.
+         *
+         * @param maxSize maximum number of members that this group can hold
+         * @return this
+         */
+        public Builder withMaxSize(int maxSize) {
+            checkArgument(maxSize >= 0, "maxSize cannot be negative");
+            this.maxSize = maxSize;
             return this;
         }
 
@@ -178,10 +266,119 @@
          * @return action profile group
          */
         public PiActionProfileGroup build() {
-            checkNotNull(id);
-            checkNotNull(piActionProfileId);
+            checkNotNull(groupId);
+            checkNotNull(actionProfileId);
+            checkArgument(maxSize == 0 || members.size() <= maxSize,
+                          "The number of members cannot exceed maxSize");
+            final boolean validActionProfileId = members.isEmpty() || members.values()
+                    .stream().allMatch(m -> m.instance() == null || m.instance()
+                            .actionProfile().equals(actionProfileId));
+            checkArgument(
+                    validActionProfileId,
+                    "The members' action profile ID must match the group one");
             return new PiActionProfileGroup(
-                    id, ImmutableSet.copyOf(members.values()), piActionProfileId);
+                    groupId, ImmutableMap.copyOf(members), actionProfileId, maxSize);
+        }
+    }
+
+    /**
+     * Weighted reference to an action profile member as used in an action
+     * profile group.
+     */
+    public static final class WeightedMember {
+
+        public static final int DEFAULT_WEIGHT = 1;
+
+        private final PiActionProfileMemberId memberId;
+        private final int weight;
+        private final PiActionProfileMember memberInstance;
+
+        /**
+         * Creates a new reference for the given action profile member ID and
+         * weight.
+         *
+         * @param memberId action profile member ID
+         * @param weight   weight
+         */
+        public WeightedMember(PiActionProfileMemberId memberId, int weight) {
+            checkNotNull(memberId);
+            this.memberId = memberId;
+            this.weight = weight;
+            this.memberInstance = null;
+        }
+
+        /**
+         * Creates a new reference from the given action profile member instance
+         * and weight. This constructor should be used when performing one-shot
+         * group programming (see {@link #instance()}).
+         *
+         * @param memberInstance action profile member instance
+         * @param weight         weight
+         */
+        public WeightedMember(PiActionProfileMember memberInstance, int weight) {
+            checkNotNull(memberInstance);
+            this.memberId = memberInstance.id();
+            this.weight = weight;
+            this.memberInstance = memberInstance;
+        }
+
+        /**
+         * Returns the ID of the action profile member.
+         *
+         * @return action profile member ID
+         */
+        public PiActionProfileMemberId id() {
+            return memberId;
+        }
+
+        /**
+         * Returns the weight of this group member.
+         *
+         * @return weight
+         */
+        public int weight() {
+            return weight;
+        }
+
+        /**
+         * If present, returns the instance of the action profile member pointed
+         * by this reference, otherwise returns null. This method is provided as
+         * a convenient way to perform one-shot group programming, and as such
+         * is meaningful only when performing write operations to a device. In
+         * other words, when reading groups from a device only the member
+         * reference should be returned and not the actual instance, hence this
+         * method should return null.
+         *
+         * @return action profile member instance, or null
+         */
+        public PiActionProfileMember instance() {
+            return memberInstance;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(memberId, weight);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            final WeightedMember other = (WeightedMember) obj;
+            return Objects.equal(this.memberId, other.memberId)
+                    && Objects.equal(this.weight, other.weight);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("memberId", memberId)
+                    .add("weight", weight)
+                    .toString();
         }
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java
index 42ed272..519a1c4 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileGroupHandle.java
@@ -34,7 +34,7 @@
 
     private PiActionProfileGroupHandle(DeviceId deviceId, PiActionProfileGroup group) {
         super(deviceId);
-        actionProfileId = group.actionProfileId();
+        actionProfileId = group.actionProfile();
         groupId = group.id();
     }
 
@@ -81,7 +81,7 @@
     public String toString() {
         return MoreObjects.toStringHelper(this)
                 .add("deviceId", deviceId())
-                .add("actionProfileId", actionProfileId)
+                .add("actionProfile", actionProfileId)
                 .add("groupId", groupId)
                 .toString();
     }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java
index 92c3562..b5df9d0 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMember.java
@@ -32,19 +32,13 @@
     private final PiActionProfileId actionProfileId;
     private final PiActionProfileMemberId memberId;
     private final PiAction action;
-    // FIXME: in P4Runtime weight is an attribute of the member reference in a
-    // group. Either remove it from this class or define the containing group
-    // ID.
-    private final int weight;
 
     private PiActionProfileMember(PiActionProfileId actionProfileId,
                                   PiActionProfileMemberId memberId,
-                                  PiAction action,
-                                  int weight) {
+                                  PiAction action) {
         this.actionProfileId = actionProfileId;
         this.memberId = memberId;
         this.action = action;
-        this.weight = weight;
     }
 
     /**
@@ -74,15 +68,6 @@
         return action;
     }
 
-    /**
-     * Returns the weight associated to this member.
-     *
-     * @return weight
-     */
-    public int weight() {
-        return weight;
-    }
-
     @Override
     public PiEntityType piEntityType() {
         return PiEntityType.ACTION_PROFILE_MEMBER;
@@ -97,15 +82,14 @@
             return false;
         }
         PiActionProfileMember that = (PiActionProfileMember) o;
-        return weight == that.weight &&
-                Objects.equal(actionProfileId, that.actionProfileId) &&
+        return Objects.equal(actionProfileId, that.actionProfileId) &&
                 Objects.equal(memberId, that.memberId) &&
                 Objects.equal(action, that.action);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(actionProfileId, memberId, action, weight);
+        return Objects.hashCode(actionProfileId, memberId, action);
     }
 
     @Override
@@ -114,7 +98,6 @@
                 .add("actionProfile", actionProfileId)
                 .add("id", memberId)
                 .add("action", action)
-                .add("weight", weight)
                 .toString();
     }
 
@@ -133,9 +116,8 @@
     public static final class Builder {
 
         private PiActionProfileId actionProfileId;
-        private PiActionProfileMemberId id;
+        private PiActionProfileMemberId memberId;
         private PiAction action;
-        private int weight;
 
         private Builder() {
             // Hides constructor.
@@ -159,7 +141,7 @@
          * @return this
          */
         public Builder withId(PiActionProfileMemberId id) {
-            this.id = id;
+            this.memberId = id;
             return this;
         }
 
@@ -175,28 +157,15 @@
         }
 
         /**
-         * Sets the weight of this member.
-         * <p>
-         * Default value is 0.
-         *
-         * @param weight weight
-         * @return this
-         */
-        public Builder withWeight(int weight) {
-            this.weight = weight;
-            return this;
-        }
-
-        /**
          * Creates a new action profile member.
          *
          * @return action profile member
          */
         public PiActionProfileMember build() {
             checkNotNull(actionProfileId);
-            checkNotNull(id);
+            checkNotNull(memberId);
             checkNotNull(action);
-            return new PiActionProfileMember(actionProfileId, id, action, weight);
+            return new PiActionProfileMember(actionProfileId, memberId, action);
         }
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java
index 8771650..40cb960 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionProfileMemberHandle.java
@@ -24,17 +24,17 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Global identifier of a PI action profile group member, uniquely defined by a
+ * Global identifier of a PI action profile member, uniquely defined by a
  * device ID, action profile ID, and member ID.
  */
 public final class PiActionProfileMemberHandle extends PiHandle<PiActionProfileMember> {
 
-    private final PiActionProfileMemberId memberId;
     private final PiActionProfileId actionProfileId;
+    private final PiActionProfileMemberId memberId;
 
     private PiActionProfileMemberHandle(DeviceId deviceId,
-                                      PiActionProfileId actionProfileId,
-                                      PiActionProfileMemberId memberId) {
+                                        PiActionProfileId actionProfileId,
+                                        PiActionProfileMemberId memberId) {
         super(deviceId);
         this.actionProfileId = actionProfileId;
         this.memberId = memberId;
@@ -119,7 +119,7 @@
     public String toString() {
         return MoreObjects.toStringHelper(this)
                 .add("deviceId", deviceId())
-                .add("actionProfileId", actionProfileId)
+                .add("actionProfile", actionProfileId)
                 .add("memberId", memberId)
                 .toString();
     }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileGroupTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileGroupTest.java
index 0fecc72..4e6a30d 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileGroupTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileGroupTest.java
@@ -16,58 +16,118 @@
 
 package org.onosproject.net.pi.runtime;
 
-import com.google.common.collect.Lists;
 import com.google.common.testing.EqualsTester;
-import org.apache.commons.collections.CollectionUtils;
 import org.junit.Test;
 import org.onosproject.net.pi.model.PiActionId;
 import org.onosproject.net.pi.model.PiActionParamId;
-
-import java.util.Collection;
+import org.onosproject.net.pi.model.PiActionProfileId;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
-import static org.onosproject.net.pi.runtime.PiConstantsTest.ACTION_PROF_ID;
+import static org.onosproject.net.pi.runtime.PiActionProfileGroup.WeightedMember.DEFAULT_WEIGHT;
 import static org.onosproject.net.pi.runtime.PiConstantsTest.DST_ADDR;
 import static org.onosproject.net.pi.runtime.PiConstantsTest.MOD_NW_DST;
+import static org.onosproject.net.pi.runtime.PiConstantsTest.MOD_VLAN_VID;
+import static org.onosproject.net.pi.runtime.PiConstantsTest.VID;
 
 /**
  * Unit tests for PiActionProfileGroup class.
  */
 public class PiActionProfileGroupTest {
 
-    private final PiActionProfileMemberId piActionProfileMemberId = PiActionProfileMemberId.of(10);
-    private final PiAction piAction = PiAction.builder().withId(PiActionId.of(MOD_NW_DST))
+    private final PiActionProfileId actionProfileId1 = PiActionProfileId.of("foo");
+    private final PiActionProfileId actionProfileId2 = PiActionProfileId.of("bar");
+
+    private final PiActionProfileGroupId groupId1 = PiActionProfileGroupId.of(100);
+    private final PiActionProfileGroupId groupId2 = PiActionProfileGroupId.of(200);
+
+    private final PiActionProfileMemberId actProfMemberId1 = PiActionProfileMemberId.of(10);
+    private final PiActionProfileMemberId actProfMemberId2 = PiActionProfileMemberId.of(20);
+
+    private final PiAction piAction1 = PiAction.builder().withId(PiActionId.of(MOD_NW_DST))
             .withParameter(new PiActionParam(PiActionParamId.of(DST_ADDR), copyFrom(0x0a010101)))
             .build();
-
-    private final PiActionProfileMember piActionProfileMember = PiActionProfileMember.builder()
-            .forActionProfile(ACTION_PROF_ID)
-            .withId(piActionProfileMemberId)
-            .withAction(piAction)
-            .withWeight(10)
-            .build();
-    private PiActionProfileGroupId piActionGroupId = PiActionProfileGroupId.of(10);
-    private PiActionProfileGroup piActionGroup1 = PiActionProfileGroup.builder()
-            .addMember(piActionProfileMember)
-            .withId(piActionGroupId)
-            .withActionProfileId(ACTION_PROF_ID)
+    private final PiAction piAction2 = PiAction.builder().withId(PiActionId.of(MOD_VLAN_VID))
+            .withParameter(new PiActionParam(PiActionParamId.of(VID), copyFrom(0x0b)))
             .build();
 
-    private PiActionProfileGroup sameAsPiActionProfileGroup1 = PiActionProfileGroup.builder()
-            .addMember(piActionProfileMember)
-            .withId(piActionGroupId)
-            .withActionProfileId(ACTION_PROF_ID)
+    private final PiActionProfileMember actProfMember11 = PiActionProfileMember.builder()
+            .forActionProfile(actionProfileId1)
+            .withId(actProfMemberId1)
+            .withAction(piAction1)
+            .build();
+    private final PiActionProfileMember actProfMember12 = PiActionProfileMember.builder()
+            .forActionProfile(actionProfileId1)
+            .withId(actProfMemberId2)
+            .withAction(piAction2)
+            .build();
+    private final PiActionProfileMember actProfMember21 = PiActionProfileMember.builder()
+            .forActionProfile(actionProfileId2)
+            .withId(actProfMemberId1)
+            .withAction(piAction1)
+            .build();
+    private final PiActionProfileMember actProfMember22 = PiActionProfileMember.builder()
+            .forActionProfile(actionProfileId2)
+            .withId(actProfMemberId2)
+            .withAction(piAction2)
             .build();
 
-    private PiActionProfileGroupId piActionGroupId2 = PiActionProfileGroupId.of(20);
-    private PiActionProfileGroup piActionGroup2 = PiActionProfileGroup.builder()
-            .addMember(piActionProfileMember)
-            .withId(piActionGroupId2)
-            .withActionProfileId(ACTION_PROF_ID)
+    private final PiActionProfileGroup.WeightedMember weightedMember1 = new PiActionProfileGroup.WeightedMember(
+            actProfMemberId1, DEFAULT_WEIGHT);
+    private final PiActionProfileGroup.WeightedMember weightedMember2 = new PiActionProfileGroup.WeightedMember(
+            actProfMemberId2, DEFAULT_WEIGHT);
+
+    private PiActionProfileGroup group1 = PiActionProfileGroup.builder()
+            .withActionProfileId(actionProfileId1)
+            // Group members defined with PiActionProfileMember instance.
+            .addMember(actProfMember11)
+            .addMember(actProfMember12)
+            .withId(groupId1)
+            .build();
+
+    private PiActionProfileGroup sameAsGroup1 = PiActionProfileGroup.builder()
+            .withActionProfileId(actionProfileId1)
+            // Group members defined with PiActionProfileMember instance, in
+            // different order.
+            .addMember(actProfMember12)
+            .addMember(actProfMember11)
+            .withId(groupId1)
+            .build();
+
+    private PiActionProfileGroup sameAsGroup1NoInstance = PiActionProfileGroup.builder()
+            .withActionProfileId(actionProfileId1)
+            // Group members defined with WeightedMember instances.
+            .addMember(weightedMember1)
+            .addMember(weightedMember2)
+            .withId(groupId1)
+            .build();
+
+    private PiActionProfileGroup group2 = PiActionProfileGroup.builder()
+            .withActionProfileId(actionProfileId2)
+            // Group members defined with PiActionProfileMember instance.
+            .addMember(actProfMember21)
+            .addMember(actProfMember22)
+            .withId(groupId2)
+            .build();
+
+    private PiActionProfileGroup sameAsGroup2NoInstance = PiActionProfileGroup.builder()
+            .withActionProfileId(actionProfileId2)
+            // Members defined by their ID only.
+            .addMember(actProfMemberId1)
+            .addMember(actProfMemberId2)
+            .withId(groupId2)
+            .build();
+
+    private PiActionProfileGroup asGroup2WithDifferentWeights = PiActionProfileGroup.builder()
+            .withActionProfileId(actionProfileId2)
+            // Members defined by their ID only and different weight.
+            .addMember(actProfMemberId1, 100)
+            .addMember(actProfMemberId2, 100)
+            .withId(groupId2)
             .build();
 
     /**
@@ -86,8 +146,9 @@
     public void testEquals() {
 
         new EqualsTester()
-                .addEqualityGroup(piActionGroup1, sameAsPiActionProfileGroup1)
-                .addEqualityGroup(piActionGroup2)
+                .addEqualityGroup(group1, sameAsGroup1, sameAsGroup1NoInstance)
+                .addEqualityGroup(group2, sameAsGroup2NoInstance)
+                .addEqualityGroup(asGroup2WithDifferentWeights)
                 .testEquals();
     }
 
@@ -96,13 +157,21 @@
      */
     @Test
     public void testMethods() {
-
-        Collection<PiActionProfileMember> piActionProfileMembers = Lists.newArrayList();
-
-        piActionProfileMembers.add(piActionProfileMember);
-        assertThat(piActionGroup1, is(notNullValue()));
-        assertThat(piActionGroup1.id(), is(piActionGroupId));
-        assertThat("Incorrect members value",
-                   CollectionUtils.isEqualCollection(piActionGroup1.members(), piActionProfileMembers));
+        assertThat(group1, is(notNullValue()));
+        assertThat(group1.id(), is(groupId1));
+        assertThat(group1.actionProfile(), is(actionProfileId1));
+        assertThat(group1.members().size(), is(2));
+        // Check members (with instance)
+        assertThat(group1.members().contains(weightedMember1), is(true));
+        assertThat(group1.members().contains(weightedMember2), is(true));
+        assertThat(group1.member(actProfMemberId1).isPresent(), is(notNullValue()));
+        assertThat(group1.member(actProfMemberId2).isPresent(), is(notNullValue()));
+        assertThat(group1.member(actProfMemberId1).get().instance(), is(actProfMember11));
+        assertThat(group1.member(actProfMemberId2).get().instance(), is(actProfMember12));
+        // Check members (no instance)
+        assertThat(sameAsGroup2NoInstance.member(actProfMemberId1).isPresent(), is(true));
+        assertThat(sameAsGroup2NoInstance.member(actProfMemberId2).isPresent(), is(true));
+        assertThat(sameAsGroup2NoInstance.member(actProfMemberId1).get().instance(), is(nullValue()));
+        assertThat(sameAsGroup2NoInstance.member(actProfMemberId2).get().instance(), is(nullValue()));
     }
 }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileMemberTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileMemberTest.java
index 317f322..5d8f99e 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileMemberTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiActionProfileMemberTest.java
@@ -29,6 +29,8 @@
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.onosproject.net.pi.runtime.PiConstantsTest.DST_ADDR;
 import static org.onosproject.net.pi.runtime.PiConstantsTest.MOD_NW_DST;
+import static org.onosproject.net.pi.runtime.PiConstantsTest.MOD_VLAN_VID;
+import static org.onosproject.net.pi.runtime.PiConstantsTest.VID;
 
 /**
  * Unit tests for PiActionProfileMember class.
@@ -37,34 +39,34 @@
 
     private final PiActionProfileId actionProfileId1 = PiActionProfileId.of("foo");
     private final PiActionProfileId actionProfileId2 = PiActionProfileId.of("bar");
-    private final PiActionProfileMemberId piActionProfileMemberId = PiActionProfileMemberId.of(10);
-    private final PiAction piAction = PiAction.builder().withId(PiActionId.of(MOD_NW_DST))
+    private final PiActionProfileMemberId piActionProfileMemberId1 = PiActionProfileMemberId.of(10);
+    private final PiActionProfileMemberId piActionProfileMemberId2 = PiActionProfileMemberId.of(20);
+    private final PiAction piAction1 = PiAction.builder().withId(PiActionId.of(MOD_NW_DST))
             .withParameter(new PiActionParam(PiActionParamId.of(DST_ADDR), copyFrom(0x0a010101)))
             .build();
+    private final PiAction piAction2 = PiAction.builder().withId(PiActionId.of(MOD_VLAN_VID))
+            .withParameter(new PiActionParam(PiActionParamId.of(VID), copyFrom(0x0b)))
+            .build();
 
     private final PiActionProfileMember piActionProfileMember1 = PiActionProfileMember.builder()
             .forActionProfile(actionProfileId1)
-            .withId(piActionProfileMemberId)
-            .withAction(piAction)
-            .withWeight(10)
+            .withId(piActionProfileMemberId1)
+            .withAction(piAction1)
             .build();
     private final PiActionProfileMember sameAsPiActionProfileMember1 = PiActionProfileMember.builder()
             .forActionProfile(actionProfileId1)
-            .withId(piActionProfileMemberId)
-            .withAction(piAction)
-            .withWeight(10)
+            .withId(piActionProfileMemberId1)
+            .withAction(piAction1)
             .build();
     private final PiActionProfileMember piActionProfileMember2 = PiActionProfileMember.builder()
             .forActionProfile(actionProfileId1)
-            .withId(piActionProfileMemberId)
-            .withAction(piAction)
-            .withWeight(20)
+            .withId(piActionProfileMemberId2)
+            .withAction(piAction2)
             .build();
-    private final PiActionProfileMember piActionGroupMember1ForOtherProfile = PiActionProfileMember.builder()
+    private final PiActionProfileMember piActionProfileMember3 = PiActionProfileMember.builder()
             .forActionProfile(actionProfileId2)
-            .withId(piActionProfileMemberId)
-            .withAction(piAction)
-            .withWeight(10)
+            .withId(piActionProfileMemberId1)
+            .withAction(piAction1)
             .build();
 
     /**
@@ -85,7 +87,7 @@
         new EqualsTester()
                 .addEqualityGroup(piActionProfileMember1, sameAsPiActionProfileMember1)
                 .addEqualityGroup(piActionProfileMember2)
-                .addEqualityGroup(piActionGroupMember1ForOtherProfile)
+                .addEqualityGroup(piActionProfileMember3)
                 .testEquals();
     }
 
@@ -96,8 +98,7 @@
     public void testMethods() {
 
         assertThat(piActionProfileMember1, is(notNullValue()));
-        assertThat(piActionProfileMember1.weight(), is(10));
-        assertThat(piActionProfileMember1.id(), is(piActionProfileMemberId));
-        assertThat(piActionProfileMember1.action(), is(piAction));
+        assertThat(piActionProfileMember1.id(), is(piActionProfileMemberId1));
+        assertThat(piActionProfileMember1.action(), is(piAction1));
     }
 }
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
index 38efdb9..e910116 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
@@ -37,6 +37,7 @@
 import org.onosproject.net.pi.model.PiPipelineModel;
 import org.onosproject.net.pi.model.PiTableId;
 import org.onosproject.net.pi.model.PiTableModel;
+import org.onosproject.net.pi.model.PiTableType;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionParam;
 import org.onosproject.net.pi.runtime.PiExactFieldMatch;
@@ -227,9 +228,24 @@
         switch (piTableAction.type()) {
             case ACTION:
                 return checkPiAction((PiAction) piTableAction, table);
-            default:
-                // FIXME: should we check? how?
+            case ACTION_PROFILE_GROUP_ID:
+            case ACTION_PROFILE_MEMBER_ID:
+                if (!table.tableType().equals(PiTableType.INDIRECT)) {
+                    throw new PiTranslationException(format(
+                            "action is indirect of type '%s', but table '%s' is of type '%s'",
+                            piTableAction.type(), table.id(), table.tableType()));
+                }
+                if (piTableAction.type().equals(PiTableAction.Type.ACTION_PROFILE_GROUP_ID)
+                        && (table.actionProfile() == null || !table.actionProfile().hasSelector())) {
+                    throw new PiTranslationException(format(
+                            "action is of type '%s', but table '%s' does not" +
+                                    "implement an action profile with dynamic selection",
+                            piTableAction.type(), table.id()));
+                }
                 return piTableAction;
+            default:
+                throw new PiTranslationException(format(
+                        "Unknown table action type %s", piTableAction.type()));
 
         }
     }
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
index de32b60..5302856 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
@@ -21,6 +21,8 @@
 import org.onosproject.net.group.Group;
 import org.onosproject.net.group.GroupBucket;
 import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiActionProfileModel;
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
 import org.onosproject.net.pi.runtime.PiAction;
@@ -55,7 +57,8 @@
     }
 
     /**
-     * Returns a PI action profile group equivalent to the given group, for the given pipeconf and device.
+     * Returns a PI action profile group equivalent to the given group, for the
+     * given pipeconf and device.
      *
      * @param group    group
      * @param pipeconf pipeconf
@@ -71,60 +74,90 @@
                     "group type %s not supported", group.type()));
         }
 
-        final PiPipelineInterpreter interpreter = getInterpreterOrNull(device, pipeconf);
-
-        final PiActionProfileGroup.Builder piActionGroupBuilder = PiActionProfileGroup.builder()
-                .withId(PiActionProfileGroupId.of(group.id().id()));
-
+        // Get action profile from group key.
+        // TODO: define proper field in group class.
         if (!(group.appCookie() instanceof PiGroupKey)) {
-            throw new PiTranslationException("group app cookie is not PI (class should be PiGroupKey)");
+            throw new PiTranslationException(
+                    "group app cookie is not PI (class should be PiGroupKey)");
         }
         final PiGroupKey groupKey = (PiGroupKey) group.appCookie();
+        final PiActionProfileId actionProfileId = groupKey.actionProfileId();
 
-        piActionGroupBuilder.withActionProfileId(groupKey.actionProfileId());
+        // Check validity of action profile against pipeconf.
+        final PiActionProfileModel actionProfileModel = pipeconf.pipelineModel()
+                .actionProfiles(actionProfileId)
+                .orElseThrow(() -> new PiTranslationException(format(
+                        "no such action profile '%s'", actionProfileId)));
+        if (!actionProfileModel.hasSelector()) {
+            throw new PiTranslationException(format(
+                    "action profile '%s' does not support dynamic selection",
+                    actionProfileId));
+        }
+
+        // Check group validity.
+        if (actionProfileModel.maxGroupSize() > 0
+                && group.buckets().buckets().size() > actionProfileModel.maxGroupSize()) {
+            throw new PiTranslationException(format(
+                    "too many buckets, max group size for action profile '%s' is %d",
+                    actionProfileId, actionProfileModel.maxGroupSize()));
+        }
+
+        final PiActionProfileGroup.Builder piActionGroupBuilder = PiActionProfileGroup.builder()
+                .withId(PiActionProfileGroupId.of(group.id().id()))
+                .withActionProfileId(groupKey.actionProfileId())
+                // We set the maximum group size as specified in the model,
+                // however this might be highly inefficient for some HW targets
+                // which pre-allocate resources for the whole group.
+                .withMaxSize(actionProfileModel.maxGroupSize());
 
         // Translate group buckets to PI group members
+        final PiPipelineInterpreter interpreter = getInterpreterOrNull(device, pipeconf);
         short bucketIdx = 0;
         for (GroupBucket bucket : group.buckets().buckets()) {
             /*
             FIXME: the way member IDs are computed can cause collisions!
-            Problem:
-            In P4Runtime action profile members, i.e. action buckets, are associated to a numeric ID chosen
-            at member insertion time. This ID must be unique for the whole action profile (i.e. the group table in
-            OpenFlow). In ONOS, GroupBucket doesn't specify any ID.
+            Problem: In P4Runtime action profile members, i.e. action buckets,
+            are associated to a numeric ID chosen at member insertion time. This
+            ID must be unique for the whole action profile (i.e. the group table
+            in OpenFlow). In ONOS, GroupBucket doesn't specify any ID.
 
             Solutions:
-            - Change GroupBucket API to force application wanting to perform group operations to specify a member id.
-            - Maintain state to dynamically allocate/deallocate member IDs, e.g. in a dedicated service, or in a
-            P4Runtime Group Provider.
+            - Change GroupBucket API to force application wanting to perform
+            group operations to specify a member id.
+            - Maintain state to dynamically allocate/deallocate member IDs, e.g.
+            in a dedicated service, or in a P4Runtime Group Provider.
 
-            Hack:
-            Statically derive member ID by combining groupId and position of the bucket in the list.
+            Hack: Statically derive member ID by combining groupId and position
+            of the bucket in the list.
              */
-            ByteBuffer bb = ByteBuffer.allocate(4)
+            final ByteBuffer bb = ByteBuffer.allocate(4)
                     .putShort((short) (group.id().id() & 0xffff))
                     .putShort(bucketIdx);
             bb.rewind();
-            int memberId = bb.getInt();
+            final int memberId = bb.getInt();
             bucketIdx++;
 
-            final PiTableAction tableAction = translateTreatment(bucket.treatment(), interpreter, groupKey.tableId(),
-                                                                 pipeconf.pipelineModel());
+            final PiTableAction tableAction = translateTreatment(
+                    bucket.treatment(), interpreter,
+                    groupKey.tableId(), pipeconf.pipelineModel());
             if (tableAction == null) {
-                throw new PiTranslationException("The PI table action returned by the interpreter is null");
+                throw new PiTranslationException(
+                        "bucket treatment translator returned null");
             }
 
             if (tableAction.type() != ACTION) {
                 throw new PiTranslationException(format(
-                        "PI table action of type %s is not supported in groups", tableAction.type()));
+                        "action of type '%s' cannot be used in action profile members",
+                        tableAction.type()));
             }
 
-            piActionGroupBuilder.addMember(PiActionProfileMember.builder()
-                                                   .forActionProfile(groupKey.actionProfileId())
-                                                   .withId(PiActionProfileMemberId.of(memberId))
-                                                   .withAction((PiAction) tableAction)
-                                                   .withWeight(bucket.weight())
-                                                   .build());
+            final PiActionProfileMember member = PiActionProfileMember.builder()
+                    .forActionProfile(groupKey.actionProfileId())
+                    .withId(PiActionProfileMemberId.of(memberId))
+                    .withAction((PiAction) tableAction)
+                    .build();
+
+            piActionGroupBuilder.addMember(member, bucket.weight());
         }
 
         return piActionGroupBuilder.build();
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
index adc96a0..fd445d7 100644
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
@@ -48,12 +48,15 @@
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.onosproject.net.group.GroupDescription.Type.SELECT;
+import static org.onosproject.net.pi.runtime.PiActionProfileGroup.WeightedMember.DEFAULT_WEIGHT;
 import static org.onosproject.pipelines.basic.BasicConstants.INGRESS_WCMP_CONTROL_SET_EGRESS_PORT;
 import static org.onosproject.pipelines.basic.BasicConstants.INGRESS_WCMP_CONTROL_WCMP_SELECTOR;
 import static org.onosproject.pipelines.basic.BasicConstants.INGRESS_WCMP_CONTROL_WCMP_TABLE;
@@ -80,16 +83,21 @@
     private static final int DEFAULT_MEMBER_WEIGHT = 1;
     private static final int BASE_MEM_ID = 65535;
     private static final int PORT_BITWIDTH = 9;
-    private Collection<PiActionProfileMember> expectedMembers;
+    private Collection<PiActionProfileMember> expectedMemberInstances;
+    private Collection<PiActionProfileGroup.WeightedMember> expectedWeightedMembers;
 
     private PiPipeconf pipeconf;
 
     @Before
     public void setUp() throws Exception {
         pipeconf = PipeconfLoader.BASIC_PIPECONF;
-        expectedMembers = ImmutableSet.of(outputMember(1),
-                                          outputMember(2),
-                                          outputMember(3));
+        expectedMemberInstances = ImmutableSet.of(outputMember(1),
+                                                  outputMember(2),
+                                                  outputMember(3));
+        expectedWeightedMembers = expectedMemberInstances.stream()
+                .map(m -> new PiActionProfileGroup.WeightedMember(m, DEFAULT_WEIGHT))
+                .collect(Collectors.toSet());
+
     }
 
     private static GroupBucket selectOutputBucket(int portNum) {
@@ -114,7 +122,6 @@
                 .forActionProfile(INGRESS_WCMP_CONTROL_WCMP_SELECTOR)
                 .withAction(piAction)
                 .withId(PiActionProfileMemberId.of(BASE_MEM_ID + portNum))
-                .withWeight(DEFAULT_MEMBER_WEIGHT)
                 .build();
     }
 
@@ -134,13 +141,22 @@
         assertThat("Group ID must be equal",
                    piGroup1.id().id(), is(equalTo(GROUP_ID.id())));
         assertThat("Action profile ID must be equal",
-                   piGroup1.actionProfileId(), is(equalTo(INGRESS_WCMP_CONTROL_WCMP_SELECTOR)));
+                   piGroup1.actionProfile(), is(equalTo(INGRESS_WCMP_CONTROL_WCMP_SELECTOR)));
 
         // members installed
-        Collection<PiActionProfileMember> members = piGroup1.members();
+        Collection<PiActionProfileGroup.WeightedMember> weightedMembers = piGroup1.members();
+        Collection<PiActionProfileMember> memberInstances = weightedMembers.stream()
+                .map(PiActionProfileGroup.WeightedMember::instance)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
         assertThat("The number of group members must be equal",
-                   piGroup1.members().size(), is(expectedMembers.size()));
-        assertThat("Group members must be equal",
-                   members.containsAll(expectedMembers) && expectedMembers.containsAll(members));
+                   piGroup1.members().size(), is(expectedWeightedMembers.size()));
+        assertThat("Group weighted members must be equal",
+                   weightedMembers.containsAll(expectedWeightedMembers)
+                           && expectedWeightedMembers.containsAll(weightedMembers));
+        assertThat("Group member instances must be equal",
+                   memberInstances.containsAll(expectedMemberInstances)
+                           && expectedMemberInstances.containsAll(memberInstances));
+
     }
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java
index 4a48897..d614d6e 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeActionGroupProgrammable.java
@@ -19,14 +19,12 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Striped;
 import org.onlab.util.SharedExecutors;
 import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileGroupMirror;
 import org.onosproject.drivers.p4runtime.mirror.P4RuntimeActionProfileMemberMirror;
 import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
-import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.group.DefaultGroup;
 import org.onosproject.net.group.DefaultGroupDescription;
@@ -36,10 +34,8 @@
 import org.onosproject.net.group.GroupOperations;
 import org.onosproject.net.group.GroupProgrammable;
 import org.onosproject.net.group.GroupStore;
-import org.onosproject.net.pi.model.PiActionId;
 import org.onosproject.net.pi.model.PiActionProfileId;
 import org.onosproject.net.pi.model.PiActionProfileModel;
-import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionProfileGroup;
 import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
 import org.onosproject.net.pi.runtime.PiActionProfileMember;
@@ -59,8 +55,9 @@
 import java.util.Set;
 import java.util.concurrent.locks.Lock;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toMap;
 import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
 import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
 import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.MODIFY;
@@ -77,7 +74,6 @@
     // the ONOS store.
     private static final String READ_ACTION_GROUPS_FROM_MIRROR = "actionGroupReadFromMirror";
     private static final boolean DEFAULT_READ_ACTION_GROUPS_FROM_MIRROR = false;
-    private static final String MAX_MEM_SIZE = "maxMemSize";
 
     protected GroupStore groupStore;
     private P4RuntimeActionProfileGroupMirror groupMirror;
@@ -86,7 +82,6 @@
 
     // Needed to synchronize operations over the same group.
     private static final Striped<Lock> STRIPED_LOCKS = Striped.lock(30);
-    private static final int GROUP_MEMBERS_BUFFER_SIZE = 3;
 
     @Override
     protected boolean setupBehaviour() {
@@ -110,13 +105,20 @@
         groupOps.operations().stream()
                 .filter(op -> !op.groupType().equals(GroupDescription.Type.ALL))
                 .forEach(op -> {
-                    // ONOS-7785 We need app cookie (action profile id) from the group
+                    // ONOS-7785 We need the group app cookie (which includes
+                    // the action profile ID) but this is not part of the
+                    // GroupDescription.
                     Group groupOnStore = groupStore.getGroup(deviceId, op.groupId());
+                    if (groupOnStore == null) {
+                        log.warn("Unable to find group {} in store, aborting {} operation",
+                                 op.groupId(), op.opType());
+                        return;
+                    }
                     GroupDescription groupDesc = new DefaultGroupDescription(
                             deviceId, op.groupType(), op.buckets(), groupOnStore.appCookie(),
                             op.groupId().id(), groupOnStore.appId());
                     DefaultGroup groupToApply = new DefaultGroup(op.groupId(), groupDesc);
-                    processGroupOperation(groupToApply, op.opType());
+                    processPdGroup(groupToApply, op.opType());
                 });
     }
 
@@ -125,89 +127,66 @@
         if (!setupBehaviour()) {
             return Collections.emptyList();
         }
-        return getActionGroups();
-    }
-
-    private Collection<Group> getActionGroups() {
 
         if (driverBoolProperty(READ_ACTION_GROUPS_FROM_MIRROR,
                                DEFAULT_READ_ACTION_GROUPS_FROM_MIRROR)) {
-            return getActionGroupsFromMirror();
+            return getGroupsFromMirror();
         }
 
-        final Collection<PiActionProfileId> actionProfileIds = pipeconf.pipelineModel()
+        // Dump groups and members from device for all action profiles.
+        final Set<PiActionProfileId> actionProfileIds = pipeconf.pipelineModel()
                 .actionProfiles()
                 .stream()
                 .map(PiActionProfileModel::id)
-                .collect(Collectors.toList());
-        final List<PiActionProfileGroup> groupsOnDevice = actionProfileIds.stream()
-                .flatMap(this::streamGroupsFromDevice)
-                .collect(Collectors.toList());
-        final Set<PiActionProfileMemberHandle> membersOnDevice = actionProfileIds
-                .stream()
-                .flatMap(actProfId -> getMembersFromDevice(actProfId)
-                        .stream()
-                        .map(memberId -> PiActionProfileMemberHandle.of(
-                                deviceId, actProfId, memberId)))
                 .collect(Collectors.toSet());
-
-        if (groupsOnDevice.isEmpty()) {
-            return Collections.emptyList();
-        }
+        final Map<PiActionProfileGroupHandle, PiActionProfileGroup>
+                groupsOnDevice = dumpAllGroupsFromDevice(actionProfileIds);
+        final Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice =
+                dumpAllMembersFromDevice(actionProfileIds);
 
         // Sync mirrors.
-        syncGroupMirror(groupsOnDevice);
-        syncMemberMirror(membersOnDevice);
+        groupMirror.sync(deviceId, groupsOnDevice);
+        memberMirror.sync(deviceId, membersOnDevice);
 
+        // Retrieve the original PD group before translation.
         final List<Group> result = Lists.newArrayList();
-        final List<PiActionProfileGroup> inconsistentGroups = Lists.newArrayList();
-        final List<PiActionProfileGroup> validGroups = Lists.newArrayList();
-
-        for (PiActionProfileGroup piGroup : groupsOnDevice) {
-            final Group pdGroup = forgeGroupEntry(piGroup);
+        final List<PiActionProfileGroup> groupsToRemove = Lists.newArrayList();
+        final Set<PiActionProfileMemberHandle> memberHandlesToKeep = Sets.newHashSet();
+        for (PiActionProfileGroup piGroup : groupsOnDevice.values()) {
+            final Group pdGroup = checkAndForgeGroupEntry(piGroup, membersOnDevice);
             if (pdGroup == null) {
                 // Entry is on device but unknown to translation service or
                 // device mirror. Inconsistent. Mark for removal.
-                inconsistentGroups.add(piGroup);
+                groupsToRemove.add(piGroup);
             } else {
-                validGroups.add(piGroup);
                 result.add(pdGroup);
+                // Keep track of member handles used in groups.
+                piGroup.members().stream()
+                        .map(m -> PiActionProfileMemberHandle.of(
+                                deviceId, piGroup.actionProfile(), m.id()))
+                        .forEach(memberHandlesToKeep::add);
             }
         }
 
-        // Trigger clean up of inconsistent groups and members. This will also
-        // remove all members that are not used by any group, and update the
-        // mirror accordingly.
-        final Set<PiActionProfileMemberHandle> membersToKeep = validGroups.stream()
-                .flatMap(g -> g.members().stream())
-                .map(m -> PiActionProfileMemberHandle.of(deviceId, m))
-                .collect(Collectors.toSet());
-        final Set<PiActionProfileMemberHandle> inconsistentMembers = Sets.difference(
-                membersOnDevice, membersToKeep);
+        // Trigger clean up of inconsistent groups and members. This will update
+        // the mirror accordingly.
+        final Set<PiActionProfileMemberHandle> memberHandlesToRemove = Sets.difference(
+                membersOnDevice.keySet(), memberHandlesToKeep);
         SharedExecutors.getSingleThreadExecutor().execute(
                 () -> cleanUpInconsistentGroupsAndMembers(
-                        inconsistentGroups, inconsistentMembers));
+                        groupsToRemove, memberHandlesToRemove));
 
+        // Done.
         return result;
     }
 
-    private void syncGroupMirror(Collection<PiActionProfileGroup> groups) {
-        Map<PiActionProfileGroupHandle, PiActionProfileGroup> handleMap = Maps.newHashMap();
-        groups.forEach(g -> handleMap.put(PiActionProfileGroupHandle.of(deviceId, g), g));
-        groupMirror.sync(deviceId, handleMap);
-    }
-
-    private void syncMemberMirror(Collection<PiActionProfileMemberHandle> memberHandles) {
-        Map<PiActionProfileMemberHandle, PiActionProfileMember> handleMap = Maps.newHashMap();
-        memberHandles.forEach(handle -> handleMap.put(
-                handle, dummyMember(handle.actionProfileId(), handle.memberId())));
-        memberMirror.sync(deviceId, handleMap);
-    }
-
-    private Collection<Group> getActionGroupsFromMirror() {
+    private Collection<Group> getGroupsFromMirror() {
+        final Map<PiActionProfileMemberHandle, PiActionProfileMember> members =
+                memberMirror.deviceHandleMap(deviceId);
         return groupMirror.getAll(deviceId).stream()
                 .map(TimedEntry::entry)
-                .map(this::forgeGroupEntry)
+                .map(g -> checkAndForgeGroupEntry(
+                        g, members))
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
     }
@@ -219,7 +198,7 @@
                      groupsToRemove.size(), deviceId);
             groupsToRemove.forEach(piGroup -> {
                 log.debug(piGroup.toString());
-                processGroup(piGroup, null, Operation.REMOVE);
+                processPiGroup(piGroup, null, Operation.REMOVE);
             });
         }
         if (!membersToRemove.isEmpty()) {
@@ -244,44 +223,77 @@
         }
     }
 
-    private Stream<PiActionProfileGroup> streamGroupsFromDevice(PiActionProfileId actProfId) {
+    private Map<PiActionProfileGroupHandle, PiActionProfileGroup> dumpAllGroupsFromDevice(
+            Set<PiActionProfileId> actProfIds) {
         // TODO: implement P4Runtime client call to read all groups with one call
         // Good if pipeline has multiple action profiles.
-        final Collection<PiActionProfileGroup> groups = getFutureWithDeadline(
-                client.dumpActionProfileGroups(actProfId, pipeconf),
-                "dumping groups", Collections.emptyList());
-        return groups.stream();
+        return actProfIds.stream()
+                .flatMap(actProfId -> getFutureWithDeadline(
+                        client.dumpActionProfileGroups(actProfId, pipeconf),
+                        "dumping groups", Collections.emptyList()).stream())
+                .collect(toMap(g -> PiActionProfileGroupHandle.of(deviceId, g), g -> g));
     }
 
-    private List<PiActionProfileMemberId> getMembersFromDevice(PiActionProfileId actProfId) {
+    private Map<PiActionProfileMemberHandle, PiActionProfileMember> dumpAllMembersFromDevice(
+            Set<PiActionProfileId> actProfIds) {
         // TODO: implement P4Runtime client call to read all members with one call
         // Good if pipeline has multiple action profiles.
-        return getFutureWithDeadline(
-                client.dumpActionProfileMemberIds(actProfId, pipeconf),
-                "dumping action profile ids", Collections.emptyList());
+        return actProfIds.stream()
+                .flatMap(actProfId -> getFutureWithDeadline(
+                        client.dumpActionProfileMembers(actProfId, pipeconf),
+                        "dumping members", Collections.emptyList()).stream())
+                .collect(toMap(m -> PiActionProfileMemberHandle.of(deviceId, m), m -> m));
     }
 
-    private Group forgeGroupEntry(PiActionProfileGroup piGroup) {
-        final PiActionProfileGroupHandle handle = PiActionProfileGroupHandle.of(deviceId, piGroup);
+    private Group checkAndForgeGroupEntry(
+            PiActionProfileGroup piGroupOnDevice,
+            Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice) {
+        final PiActionProfileGroupHandle handle = PiActionProfileGroupHandle.of(
+                deviceId, piGroupOnDevice);
         final Optional<PiTranslatedEntity<Group, PiActionProfileGroup>>
                 translatedEntity = groupTranslator.lookup(handle);
-        final TimedEntry<PiActionProfileGroup> timedEntry = groupMirror.get(handle);
-        // Is entry consistent with our state?
+        final TimedEntry<PiActionProfileGroup> mirrorEntry = groupMirror.get(handle);
+        // Check that entry obtained from device is consistent with what is known
+        // by the translation store.
         if (!translatedEntity.isPresent()) {
-            log.warn("Group handle not found in translation store: {}", handle);
+            log.warn("Group not found in translation store: {}", handle);
             return null;
         }
-        if (!translatedEntity.get().translated().equals(piGroup)) {
-            log.warn("Group obtained from device {} is different from the one in " +
-                             "translation store: device={}, store={}",
-                     deviceId, piGroup, translatedEntity.get().translated());
+        final PiActionProfileGroup piGroupFromStore = translatedEntity.get().translated();
+        if (!piGroupFromStore.equals(piGroupOnDevice)) {
+            log.warn("Group on device {} is different from the one in " +
+                             "translation store: {} [device={}, store={}]",
+                     deviceId, handle, piGroupOnDevice, piGroupFromStore);
             return null;
         }
-        if (timedEntry == null) {
+        // Groups in P4Runtime contains only a reference to members. Check that
+        // the actual member instances in the translation store are the same
+        // found on the device.
+        if (!validateMembers(piGroupFromStore, membersOnDevice)) {
+            log.warn("Group on device {} refers to members that are different " +
+                             "than those found in translation store: {}", handle);
+            return null;
+        }
+        if (mirrorEntry == null) {
             log.warn("Group handle not found in device mirror: {}", handle);
             return null;
         }
-        return addedGroup(translatedEntity.get().original(), timedEntry.lifeSec());
+        // Check that members from device are the same as in the translated group.
+        return addedGroup(translatedEntity.get().original(), mirrorEntry.lifeSec());
+    }
+
+    private boolean validateMembers(
+            PiActionProfileGroup piGroupFromStore,
+            Map<PiActionProfileMemberHandle, PiActionProfileMember> membersOnDevice) {
+        final Collection<PiActionProfileMember> groupMembers =
+                extractAllMemberInstancesOrNull(piGroupFromStore);
+        if (groupMembers == null) {
+            return false;
+        }
+        return groupMembers.stream().allMatch(
+                memberFromStore -> memberFromStore.equals(
+                        membersOnDevice.get(
+                                PiActionProfileMemberHandle.of(deviceId, memberFromStore))));
     }
 
     private Group addedGroup(Group original, long life) {
@@ -291,7 +303,7 @@
         return forgedGroup;
     }
 
-    private void processGroupOperation(Group pdGroup, GroupOperation.Type opType) {
+    private void processPdGroup(Group pdGroup, GroupOperation.Type opType) {
         final PiActionProfileGroup piGroup;
         try {
             piGroup = groupTranslator.translate(pdGroup, pipeconf);
@@ -302,12 +314,12 @@
         }
         final Operation operation = opType.equals(GroupOperation.Type.DELETE)
                 ? Operation.REMOVE : Operation.APPLY;
-        processGroup(piGroup, pdGroup, operation);
+        processPiGroup(piGroup, pdGroup, operation);
     }
 
-    private void processGroup(PiActionProfileGroup groupToApply,
-                              Group pdGroup,
-                              Operation operation) {
+    private void processPiGroup(PiActionProfileGroup groupToApply,
+                                Group pdGroup,
+                                Operation operation) {
         final PiActionProfileGroupHandle handle = PiActionProfileGroupHandle.of(deviceId, groupToApply);
         STRIPED_LOCKS.get(handle).lock();
         try {
@@ -334,55 +346,45 @@
 
     private boolean applyGroupWithMembersOrNothing(PiActionProfileGroup group, PiActionProfileGroupHandle handle) {
         // First apply members, then group, if fails, delete members.
-        if (!applyAllMembersOrNothing(group.members())) {
+        Collection<PiActionProfileMember> members = extractAllMemberInstancesOrNull(group);
+        if (members == null) {
+            return false;
+        }
+        if (!applyAllMembersOrNothing(members)) {
             return false;
         }
         if (!applyGroup(group, handle)) {
-            deleteMembers(group.members());
+            deleteMembers(handles(members));
             return false;
         }
         return true;
     }
 
-    private boolean applyGroup(PiActionProfileGroup group, PiActionProfileGroupHandle handle) {
-        final int currentMemberSize = group.members().size();
-        if (groupMirror.get(handle) != null) {
-            String maxMemSize = "";
-            if (groupMirror.annotations(handle) != null &&
-                    groupMirror.annotations(handle).value(MAX_MEM_SIZE) != null) {
-                maxMemSize = groupMirror.annotations(handle).value(MAX_MEM_SIZE);
-            }
-            if (maxMemSize.equals("") || currentMemberSize > Integer.parseInt(maxMemSize)) {
-                deleteGroup(group, handle);
-            }
+    private boolean applyGroup(PiActionProfileGroup groupToApply, PiActionProfileGroupHandle handle) {
+        final TimedEntry<PiActionProfileGroup> groupOnDevice = groupMirror.get(handle);
+        final P4RuntimeClient.WriteOperationType opType =
+                groupOnDevice == null ? INSERT : MODIFY;
+        if (opType.equals(MODIFY) && groupToApply.equals(groupOnDevice.entry())) {
+            // Skip writing, group is unchanged.
+            return true;
         }
-
-        P4RuntimeClient.WriteOperationType opType =
-                groupMirror.get(handle) == null ? INSERT : MODIFY;
-        int currentMaxMemberSize = opType == INSERT ? (currentMemberSize + GROUP_MEMBERS_BUFFER_SIZE) : 0;
-
         final boolean success = getFutureWithDeadline(
-                client.writeActionProfileGroup(group, opType, pipeconf, currentMaxMemberSize),
+                client.writeActionProfileGroup(groupToApply, opType, pipeconf),
                 "performing action profile group " + opType, false);
         if (success) {
-            groupMirror.put(handle, group);
-            if (opType == INSERT) {
-                groupMirror.putAnnotations(handle, DefaultAnnotations
-                        .builder()
-                        .set(MAX_MEM_SIZE, Integer.toString(currentMaxMemberSize))
-                        .build());
-            }
+            groupMirror.put(handle, groupToApply);
         }
         return success;
     }
 
     private boolean deleteGroup(PiActionProfileGroup group, PiActionProfileGroupHandle handle) {
         final boolean success = getFutureWithDeadline(
-                client.writeActionProfileGroup(group, DELETE, pipeconf, 0),
+                client.writeActionProfileGroup(group, DELETE, pipeconf),
                 "performing action profile group " + DELETE, false);
         if (success) {
             groupMirror.remove(handle);
         }
+        // Orphan members will be removed at the next reconciliation cycle.
         return success;
     }
 
@@ -391,7 +393,7 @@
         if (appliedMembers.size() == members.size()) {
             return true;
         } else {
-            deleteMembers(appliedMembers);
+            deleteMembers(handles(appliedMembers));
             return false;
         }
     }
@@ -403,52 +405,67 @@
                 .collect(Collectors.toList());
     }
 
-    private boolean applyMember(PiActionProfileMember member) {
-        // If exists, modify, otherwise insert
+    private boolean applyMember(PiActionProfileMember memberToApply) {
+        // If exists, modify, otherwise insert.
         final PiActionProfileMemberHandle handle = PiActionProfileMemberHandle.of(
-                deviceId, member);
+                deviceId, memberToApply);
+        final TimedEntry<PiActionProfileMember> memberOnDevice = memberMirror.get(handle);
         final P4RuntimeClient.WriteOperationType opType =
-                memberMirror.get(handle) == null ? INSERT : MODIFY;
+                memberOnDevice == null ? INSERT : MODIFY;
+        if (opType.equals(MODIFY) && memberToApply.equals(memberOnDevice.entry())) {
+            // Skip writing if member is unchanged.
+            return true;
+        }
         final boolean success = getFutureWithDeadline(
-                client.writeActionProfileMembers(Collections.singletonList(member),
-                                                 opType, pipeconf),
+                client.writeActionProfileMembers(
+                        singletonList(memberToApply), opType, pipeconf),
                 "performing action profile member " + opType, false);
         if (success) {
-            memberMirror.put(handle, dummyMember(member.actionProfile(), member.id()));
+            memberMirror.put(handle, memberToApply);
         }
         return success;
     }
 
-    private void deleteMembers(Collection<PiActionProfileMember> members) {
-        members.forEach(this::deleteMember);
+    private void deleteMembers(Collection<PiActionProfileMemberHandle> handles) {
+        // TODO: improve by batching deletes.
+        handles.forEach(this::deleteMember);
     }
 
-    private void deleteMember(PiActionProfileMember member) {
-        final PiActionProfileMemberHandle handle = PiActionProfileMemberHandle.of(
-                deviceId, member);
+    private void deleteMember(PiActionProfileMemberHandle handle) {
         final boolean success = getFutureWithDeadline(
-                client.writeActionProfileMembers(Collections.singletonList(member),
-                                                 DELETE, pipeconf),
-                "performing action profile member " + DELETE, false);
+                client.removeActionProfileMembers(
+                        handle.actionProfileId(),
+                        singletonList(handle.memberId()), pipeconf),
+                "performing action profile member " + DELETE,
+                Collections.emptyList())
+                // Successful if the only member passed has been removed.
+                .size() == 1;
         if (success) {
             memberMirror.remove(handle);
         }
     }
 
-    // FIXME: this is nasty, we have to rely on a dummy member of the mirror
-    // because the PiActionProfileMember abstraction is broken, since it includes
-    // attributes that are not part of a P4Runtime member, e.g. weight.
-    // We should remove weight from the class, and have client methods that
-    // return the full PiActionProfileMember, not just the IDs.
-    private PiActionProfileMember dummyMember(
-            PiActionProfileId actionProfileId, PiActionProfileMemberId memberId) {
-        return PiActionProfileMember.builder()
-                .forActionProfile(actionProfileId)
-                .withId(memberId)
-                .withAction(PiAction.builder()
-                                    .withId(PiActionId.of("dummy"))
-                                    .build())
-                .build();
+    private Collection<PiActionProfileMemberHandle> handles(
+            Collection<PiActionProfileMember> members) {
+        return members.stream()
+                .map(m -> PiActionProfileMemberHandle.of(
+                        deviceId, m.actionProfile(), m.id()))
+                .collect(Collectors.toList());
+    }
+
+    private Collection<PiActionProfileMember> extractAllMemberInstancesOrNull(
+            PiActionProfileGroup group) {
+        final Collection<PiActionProfileMember> instances = group.members().stream()
+                .map(PiActionProfileGroup.WeightedMember::instance)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (instances.size() != group.members().size()) {
+            log.error("PiActionProfileGroup has {} member references, " +
+                              "but only {} instances were found",
+                      group.members().size(), instances.size());
+            return null;
+        }
+        return instances;
     }
 
     enum Operation {
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
index f45a4e5..e1fc1e9 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
@@ -160,7 +160,7 @@
     @Override
     public void sync(DeviceId deviceId, Map<H, E> deviceState) {
         checkNotNull(deviceId);
-        final Map<H, E> localState = getMirrorMapForDevice(deviceId);
+        final Map<H, E> localState = deviceHandleMap(deviceId);
 
         final AtomicInteger removeCount = new AtomicInteger(0);
         final AtomicInteger updateCount = new AtomicInteger(0);
@@ -202,7 +202,8 @@
                 .collect(Collectors.toSet());
     }
 
-    private Map<H, E> getMirrorMapForDevice(DeviceId deviceId) {
+    @Override
+    public Map<H, E> deviceHandleMap(DeviceId deviceId) {
         final Map<H, E> deviceMap = Maps.newHashMap();
         mirrorMap.entrySet().stream()
                 .filter(e -> e.getKey().deviceId().equals(deviceId))
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
index 51b31aa..d62bbb8 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
@@ -75,10 +75,19 @@
     void remove(H handle);
 
     /**
+     * Returns a map of handles and corresponding PI entities for the given
+     * device.
+     *
+     * @param deviceId device ID
+     * @return map of handles and corresponding PI entities
+     */
+    Map<H, E> deviceHandleMap(DeviceId deviceId);
+
+    /**
      * Stores the given annotations associating it to the given handle.
      *
-     * @param handle handle
-     * @param annotations  entry
+     * @param handle      handle
+     * @param annotations entry
      */
     void putAnnotations(H handle, Annotations annotations);
 
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
index 5d7e93e..ff00a1a 100644
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
@@ -188,9 +188,9 @@
      * Performs the given write operation for the given action profile members
      * and pipeconf.
      *
-     * @param members   action profile members
-     * @param opType    write operation type
-     * @param pipeconf  the pipeconf currently deployed on the device
+     * @param members  action profile members
+     * @param opType   write operation type
+     * @param pipeconf the pipeconf currently deployed on the device
      * @return true if the operation was successful, false otherwise
      */
     CompletableFuture<Boolean> writeActionProfileMembers(
@@ -204,19 +204,15 @@
      * @param group         the action profile group
      * @param opType        write operation type
      * @param pipeconf      the pipeconf currently deployed on the device
-     * @param maxMemberSize the maximum number of members that can be added to
-     *                      the group. This is meaningful only if it's an INSERT
-     *                      operation, otherwise its value should be 0
      * @return true if the operation was successful, false otherwise
      */
     CompletableFuture<Boolean> writeActionProfileGroup(
             PiActionProfileGroup group,
             WriteOperationType opType,
-            PiPipeconf pipeconf,
-            int maxMemberSize);
+            PiPipeconf pipeconf);
 
     /**
-     * Dumps all groups currently installed in the given action profile.
+     * Dumps all groups for a given action profile.
      *
      * @param actionProfileId the action profile id
      * @param pipeconf        the pipeconf currently deployed on the device
@@ -226,13 +222,13 @@
             PiActionProfileId actionProfileId, PiPipeconf pipeconf);
 
     /**
-     * Dumps all action profile member IDs for a given action profile.
+     * Dumps all members for a given action profile.
      *
      * @param actionProfileId action profile ID
      * @param pipeconf        pipeconf
      * @return future of list of action profile member ID
      */
-    CompletableFuture<List<PiActionProfileMemberId>> dumpActionProfileMemberIds(
+    CompletableFuture<List<PiActionProfileMember>> dumpActionProfileMembers(
             PiActionProfileId actionProfileId, PiPipeconf pipeconf);
 
     /**
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java
new file mode 100644
index 0000000..a76ab7c
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/AbstractP4RuntimeCodec.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.p4runtime.ctl;
+
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Abstract implementation of a codec that translates PI entities into P4Runtime
+ * protobuf messages and vice versa.
+ *
+ * @param <P> PI entity class
+ * @param <M> P4Runtime protobuf message class
+ */
+abstract class AbstractP4RuntimeCodec<P extends PiEntity, M extends Message> {
+
+    protected final Logger log = getLogger(this.getClass());
+
+    protected abstract M encode(P piEntity, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    protected abstract P decode(M message, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    /**
+     * Returns a P4Runtime protobuf message that is equivalent to the given PI
+     * entity for the given pipeconf.
+     *
+     * @param piEntity PI entity instance
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     * @throws CodecException if the given PI entity cannot be encoded (see
+     *                        exception message)
+     */
+    public M encode(P piEntity, PiPipeconf pipeconf)
+            throws CodecException {
+        try {
+            return encode(piEntity, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Returns a PI entity instance that is equivalent to the P4Runtime protobuf
+     * message for the given pipeconf.
+     *
+     * @param message  P4Runtime protobuf message
+     * @param pipeconf pipeconf pipeconf
+     * @return PI entity instance
+     * @throws CodecException if the given protobuf message cannot be decoded
+     *                        (see exception message)
+     */
+    public P decode(M message, PiPipeconf pipeconf)
+            throws CodecException {
+        try {
+            return decode(message, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Same as {@link #encode(PiEntity, PiPipeconf)} but returns null in case of
+     * exceptions, while the error message is logged.
+     *
+     * @param piEntity PI entity instance
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     */
+    public M encodeOrNull(P piEntity, PiPipeconf pipeconf) {
+        try {
+            return encode(piEntity, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to encode {}: {} [{}]",
+                      piEntity.getClass().getSimpleName(),
+                      e.getMessage(), piEntity.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Same as {@link #decode(Message, PiPipeconf)} but returns null in case of
+     * exceptions, while the error message is logged.
+     *
+     * @param message  P4Runtime protobuf message
+     * @param pipeconf pipeconf pipeconf
+     * @return PI entity instance
+     */
+    public P decodeOrNull(M message, PiPipeconf pipeconf) {
+        try {
+            return decode(message, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to decode {}: {} [{}]",
+                      message.getClass().getSimpleName(),
+                      e.getMessage(), TextFormat.shortDebugString(message));
+            return null;
+        }
+    }
+
+    /**
+     * Encodes the given list of PI entities, skipping those that cannot be
+     * encoded, in which case an error message is logged. For this reason, the
+     * returned list might have different size than the returned one.
+     *
+     * @param piEntities list of PI entities
+     * @param pipeconf   pipeconf
+     * @return list of P4Runtime protobuf messages
+     */
+    public List<M> encodeAll(List<P> piEntities, PiPipeconf pipeconf) {
+        checkNotNull(piEntities);
+        return piEntities.stream()
+                .map(p -> encodeOrNull(p, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Decodes the given list of P4Runtime protobuf messages, skipping those
+     * that cannot be decoded, on which case an error message is logged. For
+     * this reason, the returned list might have different size than the
+     * returned one.
+     *
+     * @param messages list of protobuf messages
+     * @param pipeconf pipeconf
+     * @return list of PI entities
+     */
+    public List<P> decodeAll(List<M> messages, PiPipeconf pipeconf) {
+        checkNotNull(messages);
+        return messages.stream()
+                .map(m -> decodeOrNull(m, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Same as {@link #encodeAll(List, PiPipeconf)} but throws an exception if
+     * one or more of the given PI entities cannot be encoded. The returned list
+     * is guaranteed to have same size and order as the given one.
+     *
+     * @param piEntities list of PI entities
+     * @param pipeconf   pipeconf
+     * @return list of protobuf messages
+     * @throws CodecException if one or more of the given PI entities cannot be
+     *                        encoded
+     */
+    public List<M> encodeAllOrFail(List<P> piEntities, PiPipeconf pipeconf)
+            throws CodecException {
+        final List<M> messages = encodeAll(piEntities, pipeconf);
+        if (piEntities.size() != messages.size()) {
+            throw new CodecException(format(
+                    "Unable to encode %d entities of %d given " +
+                            "(see previous logs for details)",
+                    piEntities.size() - messages.size(), piEntities.size()));
+        }
+        return messages;
+    }
+
+    /**
+     * Same as {@link #decodeAll(List, PiPipeconf)} but throws an exception if
+     * one or more of the given protobuf messages cannot be decoded. The
+     * returned list is guaranteed to have same size and order as the given
+     * one.
+     *
+     * @param messages list of protobuf messages
+     * @param pipeconf pipeconf
+     * @return list of PI entities
+     * @throws CodecException if one or more of the given protobuf messages
+     *                        cannot be decoded
+     */
+    public List<P> decodeAllOrFail(List<M> messages, PiPipeconf pipeconf)
+            throws CodecException {
+        final List<P> piEntities = decodeAll(messages, pipeconf);
+        if (messages.size() != piEntities.size()) {
+            throw new CodecException(format(
+                    "Unable to decode %d messages of %d given " +
+                            "(see previous logs for details)",
+                    messages.size() - piEntities.size(), messages.size()));
+        }
+        return piEntities;
+    }
+
+    private P4InfoBrowser browserOrFail(PiPipeconf pipeconf) throws CodecException {
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new CodecException(format(
+                    "Unable to get P4InfoBrowser for pipeconf %s", pipeconf.id()));
+        }
+        return browser;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java
new file mode 100644
index 0000000..684ef04
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupCodec.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.p4runtime.ctl;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroup;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
+
+/**
+ * Codec for P4Runtime ActionProfileGroup.
+ */
+final class ActionProfileGroupCodec
+        extends AbstractP4RuntimeCodec<PiActionProfileGroup, ActionProfileGroup> {
+
+    @Override
+    public ActionProfileGroup encode(
+            PiActionProfileGroup piGroup, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+
+        final int p4ActionProfileId = browser.actionProfiles()
+                .getByName(piGroup.actionProfile().id())
+                .getPreamble().getId();
+        final ActionProfileGroup.Builder msgBuilder = ActionProfileGroup.newBuilder()
+                .setGroupId(piGroup.id().id())
+                .setActionProfileId(p4ActionProfileId)
+                .setMaxSize(piGroup.maxSize());
+        piGroup.members().forEach(m -> {
+            // TODO: currently we don't set "watch" field
+            ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
+                    .setMemberId(m.id().id())
+                    .setWeight(m.weight())
+                    .build();
+            msgBuilder.addMembers(member);
+        });
+
+        return msgBuilder.build();
+    }
+
+    @Override
+    public PiActionProfileGroup decode(
+            ActionProfileGroup msg, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+
+        final PiActionProfileGroup.Builder piGroupBuilder = PiActionProfileGroup.builder()
+                .withActionProfileId(PiActionProfileId.of(
+                        browser.actionProfiles()
+                                .getById(msg.getActionProfileId())
+                                .getPreamble().getName()))
+                .withId(PiActionProfileGroupId.of(msg.getGroupId()))
+                .withMaxSize(msg.getMaxSize());
+
+        msg.getMembersList().forEach(m -> {
+            int weight = m.getWeight();
+            if (weight < 1) {
+                // FIXME: currently PI has a bug which will always return weight 0
+                // ONOS won't accept group buckets with weight 0
+                log.warn("Decoding ActionProfileGroup with 'weight' " +
+                                 "field {}, will set to 1", weight);
+                weight = 1;
+            }
+            piGroupBuilder.addMember(PiActionProfileMemberId.of(
+                    m.getMemberId()), weight);
+        });
+        return piGroupBuilder.build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
deleted file mode 100644
index 414a0e6..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileGroupEncoder.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed 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.onosproject.p4runtime.ctl;
-
-import com.google.common.collect.Maps;
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileGroup;
-import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
-import p4.config.v1.P4InfoOuterClass;
-import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
-import p4.v1.P4RuntimeOuterClass.ActionProfileGroup.Member;
-import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
-
-import java.util.Collection;
-import java.util.Map;
-
-import static java.lang.String.format;
-
-/**
- * Encoder/Decoder for action profile group.
- */
-final class ActionProfileGroupEncoder {
-
-    private ActionProfileGroupEncoder() {
-        // hide default constructor
-    }
-
-    /**
-     * Encode a PI action profile group to a action profile group.
-     *
-     * @param piActionGroup the action profile group
-     * @param pipeconf      the pipeconf
-     * @param maxMemberSize the max member size of action group
-     * @return a action profile group encoded from PI action profile group
-     * @throws P4InfoBrowser.NotFoundException if can't find action profile from
-     *                                         P4Info browser
-     * @throws EncodeException                 if can't find P4Info from
-     *                                         pipeconf
-     */
-    static ActionProfileGroup encode(PiActionProfileGroup piActionGroup, PiPipeconf pipeconf, int maxMemberSize)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-
-        PiActionProfileId piActionProfileId = piActionGroup.actionProfileId();
-        P4InfoOuterClass.ActionProfile actionProfile = browser.actionProfiles()
-                .getByName(piActionProfileId.id());
-        int actionProfileId = actionProfile.getPreamble().getId();
-        ActionProfileGroup.Builder actionProfileGroupBuilder = ActionProfileGroup.newBuilder()
-                .setGroupId(piActionGroup.id().id())
-                .setActionProfileId(actionProfileId);
-
-        piActionGroup.members().forEach(m -> {
-            // TODO: currently we don't set "watch" field of member
-            Member member = Member.newBuilder()
-                    .setMemberId(m.id().id())
-                    .setWeight(m.weight())
-                    .build();
-            actionProfileGroupBuilder.addMembers(member);
-        });
-
-        if (maxMemberSize > 0) {
-            actionProfileGroupBuilder.setMaxSize(maxMemberSize);
-        }
-
-        return actionProfileGroupBuilder.build();
-    }
-
-    /**
-     * Decode an action profile group with members information to a PI action
-     * profile group.
-     *
-     * @param actionProfileGroup the action profile group
-     * @param members            members of the action profile group
-     * @param pipeconf           the pipeconf
-     * @return decoded PI action profile group
-     * @throws P4InfoBrowser.NotFoundException if can't find action profile from
-     *                                         P4Info browser
-     * @throws EncodeException                 if can't find P4Info from
-     *                                         pipeconf
-     */
-    static PiActionProfileGroup decode(ActionProfileGroup actionProfileGroup,
-                                       Collection<ActionProfileMember> members,
-                                       PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-        PiActionProfileGroup.Builder piActionGroupBuilder = PiActionProfileGroup.builder();
-
-        P4InfoOuterClass.ActionProfile actionProfile = browser.actionProfiles()
-                .getById(actionProfileGroup.getActionProfileId());
-        PiActionProfileId piActionProfileId = PiActionProfileId.of(actionProfile.getPreamble().getName());
-
-        piActionGroupBuilder
-                .withActionProfileId(piActionProfileId)
-                .withId(PiActionProfileGroupId.of(actionProfileGroup.getGroupId()));
-
-        Map<Integer, Integer> memberWeights = Maps.newHashMap();
-        actionProfileGroup.getMembersList().forEach(member -> {
-            int weight = member.getWeight();
-            if (weight < 1) {
-                // FIXME: currently PI has a bug which will always return weight 0
-                // ONOS won't accept group buckets with weight 0
-                weight = 1;
-            }
-            memberWeights.put(member.getMemberId(), weight);
-        });
-
-        for (ActionProfileMember member : members) {
-            if (!memberWeights.containsKey(member.getMemberId())) {
-                // Not a member of this group, ignore.
-                continue;
-            }
-            int weight = memberWeights.get(member.getMemberId());
-            piActionGroupBuilder.addMember(ActionProfileMemberEncoder.decode(member, weight, pipeconf));
-        }
-
-        return piActionGroupBuilder.build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java
new file mode 100644
index 0000000..9567b0a
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberCodec.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.p4runtime.ctl;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileMember;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
+
+import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decodeActionMsg;
+import static org.onosproject.p4runtime.ctl.TableEntryEncoder.encodePiAction;
+/**
+ * Codec for P4Runtime ActionProfileMember.
+ */
+final class ActionProfileMemberCodec
+        extends AbstractP4RuntimeCodec<PiActionProfileMember, ActionProfileMember> {
+
+    @Override
+    public ActionProfileMember encode(PiActionProfileMember piEntity,
+                                      PiPipeconf pipeconf,
+                                      P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final ActionProfileMember.Builder actionProfileMemberBuilder =
+                ActionProfileMember.newBuilder();
+        // Member ID
+        actionProfileMemberBuilder.setMemberId(piEntity.id().id());
+        // Action profile ID
+        P4InfoOuterClass.ActionProfile actionProfile =
+                browser.actionProfiles().getByName(piEntity.actionProfile().id());
+        final int actionProfileId = actionProfile.getPreamble().getId();
+        actionProfileMemberBuilder.setActionProfileId(actionProfileId);
+        // Action
+        final P4RuntimeOuterClass.Action action = encodePiAction(piEntity.action(), browser);
+        actionProfileMemberBuilder.setAction(action);
+        return actionProfileMemberBuilder.build();
+    }
+
+    @Override
+    public PiActionProfileMember decode(ActionProfileMember message,
+                                        PiPipeconf pipeconf,
+                                        P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final PiActionProfileId actionProfileId = PiActionProfileId.of(
+                browser.actionProfiles()
+                        .getById(message.getActionProfileId())
+                        .getPreamble()
+                        .getName());
+        return PiActionProfileMember.builder()
+                .forActionProfile(actionProfileId)
+                .withId(PiActionProfileMemberId.of(message.getMemberId()))
+                .withAction(decodeActionMsg(message.getAction(), browser))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
deleted file mode 100644
index 30dd43c..0000000
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/ActionProfileMemberEncoder.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed 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.onosproject.p4runtime.ctl;
-
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionProfileMember;
-import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
-import p4.config.v1.P4InfoOuterClass;
-import p4.v1.P4RuntimeOuterClass;
-import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
-
-import static java.lang.String.format;
-import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decodeActionMsg;
-import static org.onosproject.p4runtime.ctl.TableEntryEncoder.encodePiAction;
-
-/**
- * Encoder/Decoder of action profile member.
- */
-final class ActionProfileMemberEncoder {
-    private ActionProfileMemberEncoder() {
-        // Hide default constructor
-    }
-
-    /**
-     * Encode a PiActionProfileMember to a ActionProfileMember.
-     *
-     * @param member    the member to encode
-     * @param pipeconf  the pipeconf, as encode spec
-     * @return encoded member
-     * @throws P4InfoBrowser.NotFoundException can't find action profile from
-     *                                         P4Info browser
-     * @throws EncodeException                 can't find P4Info from pipeconf
-     */
-    static ActionProfileMember encode(PiActionProfileMember member,
-                                      PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-
-        ActionProfileMember.Builder actionProfileMemberBuilder =
-                ActionProfileMember.newBuilder();
-
-        // member id
-        actionProfileMemberBuilder.setMemberId(member.id().id());
-
-        // action profile id
-        P4InfoOuterClass.ActionProfile actionProfile =
-                browser.actionProfiles().getByName(member.actionProfile().id());
-
-        int actionProfileId = actionProfile.getPreamble().getId();
-        actionProfileMemberBuilder.setActionProfileId(actionProfileId);
-
-        // Action
-        P4RuntimeOuterClass.Action action = encodePiAction(member.action(), browser);
-        actionProfileMemberBuilder.setAction(action);
-
-        return actionProfileMemberBuilder.build();
-    }
-
-    /**
-     * Decode an action profile member to PI action profile member.
-     *
-     * @param member   the action profile member
-     * @param weight   the weight of the member
-     * @param pipeconf the pipeconf, as decode spec
-     * @return decoded PI action profile member
-     * @throws P4InfoBrowser.NotFoundException can't find definition of action
-     *                                         from P4 info
-     * @throws EncodeException                 can't get P4 info browser from
-     *                                         pipeconf
-     */
-    static PiActionProfileMember decode(ActionProfileMember member,
-                                        int weight,
-                                        PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
-        P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
-        if (browser == null) {
-            throw new EncodeException(format("Can't get P4 info browser from pipeconf %s", pipeconf));
-        }
-
-        final PiActionProfileId actionProfileId = PiActionProfileId.of(
-                browser.actionProfiles()
-                        .getById(member.getActionProfileId())
-                        .getPreamble()
-                        .getName());
-
-        return PiActionProfileMember.builder()
-                .forActionProfile(actionProfileId)
-                .withId(PiActionProfileMemberId.of(member.getMemberId()))
-                .withWeight(weight)
-                .withAction(decodeActionMsg(member.getAction(), browser))
-                .build();
-    }
-}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/EncodeException.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
similarity index 65%
rename from protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/EncodeException.java
rename to protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
index 51abbb5..89d5510 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/EncodeException.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CodecException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-present Open Networking Foundation
+ * Copyright 2019-present Open Networking Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,11 +17,15 @@
 package org.onosproject.p4runtime.ctl;
 
 /**
- * Signals that the proto message cannot be build.
+ * Signals an error during encoding/decoding of a PI entity/protobuf message.
  */
-final class EncodeException extends Exception {
+public final class CodecException extends Exception {
 
-    EncodeException(String explanation) {
+    /**
+     * Ceeates anew exception with the given explanation message.
+     * @param explanation explanation
+     */
+    public CodecException(String explanation) {
         super(explanation);
     }
 }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
index 6c29062..3765d24 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/CounterEntryCodec.java
@@ -80,7 +80,7 @@
                 .map(cellId -> {
                     try {
                         return encodePiCounterCellId(cellId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode PI counter cell id: {}", e.getMessage());
                         return null;
                     }
@@ -114,7 +114,7 @@
                 .map(counterId -> {
                     try {
                         return readAllCellsEntity(counterId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode counter ID to read-all-cells entity: {}",
                                  e.getMessage());
                         return null;
@@ -152,7 +152,7 @@
                 .map(entity -> {
                     try {
                         return decodeCounterEntity(entity, pipeconf, browser);
-                    } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
+                    } catch (CodecException | P4InfoBrowser.NotFoundException e) {
                         log.warn("Unable to decode counter entity message: {}",
                                  e.getMessage());
                         return null;
@@ -165,7 +165,7 @@
     private static Entity encodePiCounterCellId(PiCounterCellId cellId,
                                                 PiPipeconf pipeconf,
                                                 P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int counterId;
         Entity entity;
@@ -193,7 +193,7 @@
                         .build();
                 break;
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Unrecognized PI counter cell ID type '%s'",
                         cellId.counterType()));
         }
@@ -204,10 +204,10 @@
     private static Entity readAllCellsEntity(PiCounterId counterId,
                                              PiPipeconf pipeconf,
                                              P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         if (!pipeconf.pipelineModel().counter(counterId).isPresent()) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "not such counter '%s' in pipeline model", counterId));
         }
         final PiCounterType counterType = pipeconf.pipelineModel()
@@ -228,7 +228,7 @@
                 final PiTableId tableId = pipeconf.pipelineModel()
                         .counter(counterId).get().table();
                 if (tableId == null) {
-                    throw new EncodeException(format(
+                    throw new CodecException(format(
                             "null table for direct counter '%s'", counterId));
                 }
                 final int p4TableId = browser.tables().getByName(tableId.id())
@@ -243,7 +243,7 @@
                                 .build())
                         .build();
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "unrecognized PI counter type '%s'", counterType));
         }
     }
@@ -251,7 +251,7 @@
     private static PiCounterCell decodeCounterEntity(Entity entity,
                                                      PiPipeconf pipeconf,
                                                      P4InfoBrowser browser)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         CounterData counterData;
         PiCounterCellId piCellId;
@@ -271,7 +271,7 @@
             piCellId = PiCounterCellId.ofDirect(piTableEntry);
             counterData = entity.getDirectCounterEntry().getData();
         } else {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "Unrecognized entity type '%s' in P4Runtime message",
                     entity.getEntityCase().name()));
         }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
index a825488..44f27d1 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MeterEntryCodec.java
@@ -80,7 +80,7 @@
                 .map(cellConfig -> {
                     try {
                         return encodePiMeterCellConfig(cellConfig, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode PI meter cell id: {}", e.getMessage());
                         log.debug("exception", e);
                         return null;
@@ -115,7 +115,7 @@
                 .map(meterId -> {
                     try {
                         return readAllCellsEntity(meterId, pipeconf, browser);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+                    } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                         log.warn("Unable to encode meter ID to read-all-cells entity: {}",
                                  e.getMessage());
                         return null;
@@ -152,7 +152,7 @@
                 .map(entity -> {
                     try {
                         return decodeMeterEntity(entity, pipeconf, browser);
-                    } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
+                    } catch (CodecException | P4InfoBrowser.NotFoundException e) {
                         log.warn("Unable to decode meter entity message: {}", e.getMessage());
                         return null;
                     }
@@ -164,7 +164,7 @@
     private static Entity encodePiMeterCellConfig(PiMeterCellConfig config,
                                                   PiPipeconf pipeconf,
                                                   P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int meterId;
         Entity entity;
@@ -198,7 +198,7 @@
             // When reading meter cells.
             meterConfig = null;
         } else {
-            throw new EncodeException("number of meter bands should be either 2 or 0");
+            throw new CodecException("number of meter bands should be either 2 or 0");
         }
 
         switch (config.cellId().meterType()) {
@@ -226,8 +226,8 @@
                         .setDirectMeterEntry(dirEntryBuilder.build()).build();
                 break;
             default:
-                throw new EncodeException(format("unrecognized PI meter type '%s'",
-                                                 config.cellId().meterType()));
+                throw new CodecException(format("unrecognized PI meter type '%s'",
+                                                config.cellId().meterType()));
         }
 
         return entity;
@@ -236,10 +236,10 @@
     private static Entity readAllCellsEntity(PiMeterId meterId,
                                              PiPipeconf pipeconf,
                                              P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         if (!pipeconf.pipelineModel().meter(meterId).isPresent()) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "not such meter '%s' in pipeline model", meterId));
         }
         final PiMeterType meterType = pipeconf.pipelineModel()
@@ -260,7 +260,7 @@
                 final PiTableId tableId = pipeconf.pipelineModel()
                         .meter(meterId).get().table();
                 if (tableId == null) {
-                    throw new EncodeException(format(
+                    throw new CodecException(format(
                             "null table for direct meter '%s'", meterId));
                 }
                 final int p4TableId = browser.tables().getByName(tableId.id())
@@ -275,7 +275,7 @@
                                 .build())
                         .build();
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "unrecognized PI meter type '%s'", meterType));
         }
     }
@@ -283,7 +283,7 @@
     private static PiMeterCellConfig decodeMeterEntity(Entity entity,
                                                        PiPipeconf pipeconf,
                                                        P4InfoBrowser browser)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         MeterConfig meterConfig;
         PiMeterCellId piCellId;
@@ -304,7 +304,7 @@
             piCellId = PiMeterCellId.ofDirect(piTableEntry);
             meterConfig = entity.getDirectMeterEntry().getConfig();
         } else {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "unrecognized entity type '%s' in P4Runtime message",
                     entity.getEntityCase().name()));
         }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
index f2ececb..5f55c1f 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
@@ -40,9 +40,9 @@
      *
      * @param piEntry PiMulticastGroupEntry
      * @return P4Runtime MulticastGroupEntry message
-     * @throws EncodeException if the PiMulticastGroupEntry cannot be encoded.
+     * @throws CodecException if the PiMulticastGroupEntry cannot be encoded.
      */
-    static MulticastGroupEntry encode(PiMulticastGroupEntry piEntry) throws EncodeException {
+    static MulticastGroupEntry encode(PiMulticastGroupEntry piEntry) throws CodecException {
         final MulticastGroupEntry.Builder msgBuilder = MulticastGroupEntry.newBuilder();
         msgBuilder.setMulticastGroupId(piEntry.groupId());
         for (PiPreReplica replica : piEntry.replicas()) {
@@ -50,7 +50,7 @@
             try {
                 p4PortId = Math.toIntExact(replica.egressPort().toLong());
             } catch (ArithmeticException e) {
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Cannot cast 64bit port value '%s' to 32bit",
                         replica.egressPort()));
             }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
index 78ea9d9..6ac478f 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
@@ -16,10 +16,8 @@
 
 package org.onosproject.p4runtime.ctl;
 
-import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
@@ -86,14 +84,21 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
+import java.util.stream.Stream;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.lang.String.format;
 import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static org.onosproject.p4runtime.ctl.P4RuntimeCodecs.CODECS;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_GROUP;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_MEMBER;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.COUNTER_ENTRY;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_COUNTER_ENTRY;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.DIRECT_METER_ENTRY;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.METER_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.PACKET_REPLICATION_ENGINE_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.TABLE_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.PacketIn;
@@ -106,10 +111,13 @@
  */
 final class P4RuntimeClientImpl extends AbstractGrpcClient implements P4RuntimeClient {
 
+    private static final String MISSING_P4INFO_BROWSER = "Unable to get a P4Info browser for pipeconf {}";
+
     private static final Metadata.Key<com.google.rpc.Status> STATUS_DETAILS_KEY =
-            Metadata.Key.of("grpc-status-details-bin",
-                            ProtoLiteUtils.metadataMarshaller(
-                                    com.google.rpc.Status.getDefaultInstance()));
+            Metadata.Key.of(
+                    "grpc-status-details-bin",
+                    ProtoLiteUtils.metadataMarshaller(
+                            com.google.rpc.Status.getDefaultInstance()));
 
     private static final Map<WriteOperationType, Update.Type> UPDATE_TYPES = ImmutableMap.of(
             WriteOperationType.UNSPECIFIED, Update.Type.UNSPECIFIED,
@@ -229,9 +237,8 @@
     @Override
     public CompletableFuture<Boolean> writeActionProfileGroup(PiActionProfileGroup group,
                                                               WriteOperationType opType,
-                                                              PiPipeconf pipeconf,
-                                                       int maxMemberSize) {
-        return supplyInContext(() -> doWriteActionProfileGroup(group, opType, pipeconf, maxMemberSize),
+                                                              PiPipeconf pipeconf) {
+        return supplyInContext(() -> doWriteActionProfileGroup(group, opType, pipeconf),
                                "writeActionProfileGroup-" + opType.name());
     }
 
@@ -243,10 +250,10 @@
     }
 
     @Override
-    public CompletableFuture<List<PiActionProfileMemberId>> dumpActionProfileMemberIds(
+    public CompletableFuture<List<PiActionProfileMember>> dumpActionProfileMembers(
             PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
-        return supplyInContext(() -> doDumpActionProfileMemberIds(actionProfileId, pipeconf),
-                               "dumpActionProfileMemberIds-" + actionProfileId.id());
+        return supplyInContext(() -> doDumpActionProfileMembers(actionProfileId, pipeconf),
+                               "dumpActionProfileMembers-" + actionProfileId.id());
     }
 
     @Override
@@ -484,8 +491,8 @@
                                                             .build())
                                          .setType(UPDATE_TYPES.get(opType))
                                          .build())
-                    .collect(Collectors.toList());
-        } catch (EncodeException e) {
+                    .collect(toList());
+        } catch (CodecException e) {
             log.error("Unable to encode table entries, aborting {} operation: {}",
                       opType.name(), e.getMessage());
             return false;
@@ -507,7 +514,7 @@
         } else {
             P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
             if (browser == null) {
-                log.warn("Unable to get a P4Info browser for pipeconf {}", pipeconf);
+                log.error(MISSING_P4INFO_BROWSER, pipeconf);
                 return Collections.emptyList();
             }
             piTableIds.forEach(piTableId -> {
@@ -523,36 +530,18 @@
             return Collections.emptyList();
         }
 
-        ReadRequest.Builder requestMsgBuilder = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId);
-        tableIds.forEach(tableId -> requestMsgBuilder.addEntities(
-                Entity.newBuilder()
-                        .setTableEntry(
-                                TableEntry.newBuilder()
-                                        .setTableId(tableId)
-                                        .setIsDefaultAction(defaultEntries)
-                                        .setCounterData(P4RuntimeOuterClass.CounterData.getDefaultInstance())
-                                        .build())
+        final List<Entity> entities = tableIds.stream()
+                .map(tableId ->  TableEntry.newBuilder()
+                        .setTableId(tableId)
+                        .setIsDefaultAction(defaultEntries)
+                        .setCounterData(P4RuntimeOuterClass.CounterData.getDefaultInstance())
                         .build())
-                .build());
+                .map(e -> Entity.newBuilder().setTableEntry(e).build())
+                .collect(toList());
 
-        Iterator<ReadResponse> responses;
-        try {
-            responses = blockingStub.read(requestMsgBuilder.build());
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to dump tables from {}: {}", deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        Iterable<ReadResponse> responseIterable = () -> responses;
-        List<TableEntry> tableEntryMsgs = StreamSupport
-                .stream(responseIterable.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(entity -> entity.getEntityCase() == TABLE_ENTRY)
+        final List<TableEntry> tableEntryMsgs = blockingRead(entities, TABLE_ENTRY)
                 .map(Entity::getTableEntry)
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Retrieved {} entries from {} tables on {}...",
                   tableEntryMsgs.size(), tableIds.size(), deviceId);
@@ -641,61 +630,32 @@
     private List<PiCounterCell> doReadCounterEntities(
             List<Entity> counterEntities, PiPipeconf pipeconf) {
 
-        if (counterEntities.size() == 0) {
-            return Collections.emptyList();
-        }
-
-        final ReadRequest request = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addAllEntities(counterEntities)
-                .build();
-
-        final Iterable<ReadResponse> responses;
-        try {
-            responses = () -> blockingStub.read(request);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read counter cells from {}: {}", deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        List<Entity> entities = StreamSupport.stream(responses.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .collect(Collectors.toList());
+        final List<Entity> entities = blockingRead(
+                counterEntities, COUNTER_ENTRY, DIRECT_COUNTER_ENTRY)
+                .collect(toList());
 
         return CounterEntryCodec.decodeCounterEntities(entities, pipeconf);
     }
 
     private boolean doWriteActionProfileMembers(List<PiActionProfileMember> members,
                                                 WriteOperationType opType, PiPipeconf pipeconf) {
-        final List<ActionProfileMember> actionProfileMembers = Lists.newArrayList();
-
-        for (PiActionProfileMember member : members) {
-            try {
-                actionProfileMembers.add(ActionProfileMemberEncoder.encode(member, pipeconf));
-            } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-                log.warn("Unable to encode action profile member, aborting {} operation: {} [{}]",
-                         opType.name(), e.getMessage(), member.toString());
-                return false;
-            }
+        final List<ActionProfileMember> actionProfileMembers;
+        try {
+            actionProfileMembers = CODECS.actionProfileMember()
+                    .encodeAllOrFail(members, pipeconf);
+        } catch (CodecException e) {
+            log.warn("Unable to {} action profile members: {}",
+                     opType.name(), e.getMessage());
+            return false;
         }
-
         final List<Update> updateMsgs = actionProfileMembers.stream()
-                .map(actionProfileMember ->
-                             Update.newBuilder()
-                                     .setEntity(Entity.newBuilder()
-                                                        .setActionProfileMember(actionProfileMember)
-                                                        .build())
-                                     .setType(UPDATE_TYPES.get(opType))
-                                     .build())
-                .collect(Collectors.toList());
-
-        if (updateMsgs.size() == 0) {
-            // Nothing to update.
-            return true;
-        }
-
+                .map(m -> Update.newBuilder()
+                        .setEntity(Entity.newBuilder()
+                                           .setActionProfileMember(m)
+                                           .build())
+                        .setType(UPDATE_TYPES.get(opType))
+                        .build())
+                .collect(toList());
         return write(updateMsgs, members, opType, "action profile member");
     }
 
@@ -705,7 +665,7 @@
 
         final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, aborting dump action profile", pipeconf);
+            log.warn(MISSING_P4INFO_BROWSER, pipeconf);
             return Collections.emptyList();
         }
 
@@ -721,117 +681,29 @@
             return Collections.emptyList();
         }
 
-        // Prepare read request to read all groups from the given action profile.
-        final ReadRequest groupRequestMsg = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addEntities(Entity.newBuilder()
-                                     .setActionProfileGroup(
-                                             ActionProfileGroup.newBuilder()
-                                                     .setActionProfileId(actionProfileId)
-                                                     .build())
-                                     .build())
+        // Read all groups from the given action profile.
+        final Entity entityToRead = Entity.newBuilder()
+                .setActionProfileGroup(
+                        ActionProfileGroup.newBuilder()
+                                .setActionProfileId(actionProfileId)
+                                .build())
                 .build();
-
-        // Read groups.
-        final Iterator<ReadResponse> groupResponses;
-        try {
-            groupResponses = blockingStub.read(groupRequestMsg);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to dump action profile {} from {}: {}", piActionProfileId, deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        final List<ActionProfileGroup> groupMsgs = Tools.stream(() -> groupResponses)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(entity -> entity.getEntityCase() == ACTION_PROFILE_GROUP)
+        final List<ActionProfileGroup> groupMsgs = blockingRead(entityToRead, ACTION_PROFILE_GROUP)
                 .map(Entity::getActionProfileGroup)
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Retrieved {} groups from action profile {} on {}...",
                   groupMsgs.size(), piActionProfileId.id(), deviceId);
 
-        // Returned groups contain only a minimal description of their members.
-        // We need to issue a new request to get the full description of each member.
-
-        // Keep a map of all member IDs for each group ID, will need it later.
-        final Multimap<Integer, Integer> groupIdToMemberIdsMap = HashMultimap.create();
-        groupMsgs.forEach(g -> groupIdToMemberIdsMap.putAll(
-                g.getGroupId(),
-                g.getMembersList().stream()
-                        .map(ActionProfileGroup.Member::getMemberId)
-                        .collect(Collectors.toList())));
-
-        // Prepare one big read request to read all members in one shot.
-        final Set<Entity> entityMsgs = groupMsgs.stream()
-                .flatMap(g -> g.getMembersList().stream())
-                .map(ActionProfileGroup.Member::getMemberId)
-                // Prevent issuing many read requests for the same member.
-                .distinct()
-                .map(id -> ActionProfileMember.newBuilder()
-                        .setActionProfileId(actionProfileId)
-                        .setMemberId(id)
-                        .build())
-                .map(m -> Entity.newBuilder()
-                        .setActionProfileMember(m)
-                        .build())
-                .collect(Collectors.toSet());
-        final ReadRequest memberRequestMsg = ReadRequest.newBuilder().setDeviceId(p4DeviceId)
-                .addAllEntities(entityMsgs)
-                .build();
-
-        // Read members.
-        final Iterator<ReadResponse> memberResponses;
-        try {
-            memberResponses = blockingStub.read(memberRequestMsg);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read members of action profile {} from {}: {}",
-                     piActionProfileId, deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        final Multimap<Integer, ActionProfileMember> groupIdToMembersMap = HashMultimap.create();
-        Tools.stream(() -> memberResponses)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> e.getEntityCase() == ACTION_PROFILE_MEMBER)
-                .map(Entity::getActionProfileMember)
-                .forEach(member -> groupIdToMemberIdsMap.asMap()
-                        // Get all group IDs that contain this member.
-                        .entrySet()
-                        .stream()
-                        .filter(entry -> entry.getValue().contains(member.getMemberId()))
-                        .map(Map.Entry::getKey)
-                        .forEach(gid -> groupIdToMembersMap.put(gid, member)));
-
-        log.debug("Retrieved {} members from action profile {} on {}...",
-                  groupIdToMembersMap.size(), piActionProfileId.id(), deviceId);
-
-        return groupMsgs.stream()
-                .map(groupMsg -> {
-                    try {
-                        return ActionProfileGroupEncoder.decode(groupMsg,
-                                                                groupIdToMembersMap.get(groupMsg.getGroupId()),
-                                                                pipeconf);
-                    } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
-                        log.warn("Unable to decode group: {}\n {}", e.getMessage(), groupMsg);
-                        return null;
-                    }
-                })
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+        return CODECS.actionProfileGroup().decodeAll(groupMsgs, pipeconf);
     }
 
-    private List<PiActionProfileMemberId> doDumpActionProfileMemberIds(
+    private List<PiActionProfileMember> doDumpActionProfileMembers(
             PiActionProfileId actionProfileId, PiPipeconf pipeconf) {
 
         final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, " +
-                             "aborting cleanup of action profile members",
-                     pipeconf);
+            log.error(MISSING_P4INFO_BROWSER, pipeconf);
             return Collections.emptyList();
         }
 
@@ -843,40 +715,24 @@
                     .getPreamble()
                     .getId();
         } catch (P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to cleanup action profile members: {}", e.getMessage());
+            log.warn("Unable to dump action profile members: {}", e.getMessage());
             return Collections.emptyList();
         }
 
-        final ReadRequest memberRequestMsg = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addEntities(Entity.newBuilder().setActionProfileMember(
+        Entity entityToRead = Entity.newBuilder()
+                .setActionProfileMember(
                         ActionProfileMember.newBuilder()
                                 .setActionProfileId(p4ActProfId)
-                                .build()).build())
+                                .build())
                 .build();
-
-        // Read members.
-        final Iterator<ReadResponse> memberResponses;
-        try {
-            memberResponses = blockingStub.read(memberRequestMsg);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read members of action profile {} from {}: {}",
-                     actionProfileId, deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        return Tools.stream(() -> memberResponses)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> e.getEntityCase() == ACTION_PROFILE_MEMBER)
+        final List<ActionProfileMember> memberMsgs = blockingRead(entityToRead, ACTION_PROFILE_MEMBER)
                 .map(Entity::getActionProfileMember)
-                // Perhaps not needed, but better to double check to avoid
-                // removing members of other groups.
-                .filter(m -> m.getActionProfileId() == p4ActProfId)
-                .map(ActionProfileMember::getMemberId)
-                .map(PiActionProfileMemberId::of)
-                .collect(Collectors.toList());
+                .collect(toList());
+
+        log.debug("Retrieved {} members from action profile {} on {}...",
+                  memberMsgs.size(), actionProfileId.id(), deviceId);
+
+        return CODECS.actionProfileMember().decodeAll(memberMsgs, pipeconf);
     }
 
     private List<PiActionProfileMemberId> doRemoveActionProfileMembers(
@@ -890,9 +746,7 @@
 
         final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            log.warn("Unable to get a P4Info browser for pipeconf {}, " +
-                             "aborting cleanup of action profile members",
-                     pipeconf);
+            log.error(MISSING_P4INFO_BROWSER, pipeconf);
             return Collections.emptyList();
         }
 
@@ -912,7 +766,7 @@
                 .map(m -> Entity.newBuilder().setActionProfileMember(m).build())
                 .map(e -> Update.newBuilder().setEntity(e)
                         .setType(Update.Type.DELETE).build())
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Removing {} members of action profile '{}'...",
                   memberIds.size(), actionProfileId);
@@ -923,23 +777,19 @@
     }
 
     private boolean doWriteActionProfileGroup(
-            PiActionProfileGroup group, WriteOperationType opType, PiPipeconf pipeconf,
-                                       int maxMemberSize) {
-        final ActionProfileGroup actionProfileGroup;
-        if (opType == P4RuntimeClient.WriteOperationType.INSERT && maxMemberSize < group.members().size()) {
-            log.warn("Unable to encode group, since group member larger than maximum member size");
-            return false;
-        }
+            PiActionProfileGroup group, WriteOperationType opType, PiPipeconf pipeconf) {
+        final ActionProfileGroup groupMsg;
         try {
-            actionProfileGroup = ActionProfileGroupEncoder.encode(group, pipeconf, maxMemberSize);
-        } catch (EncodeException | P4InfoBrowser.NotFoundException e) {
-            log.warn("Unable to encode group, aborting {} operation: {}", e.getMessage(), opType.name());
+            groupMsg = CODECS.actionProfileGroup().encode(group, pipeconf);
+        } catch (CodecException e) {
+            log.warn("Unable to encode group, aborting {} operation: {}",
+                     opType.name(), e.getMessage());
             return false;
         }
 
         final Update updateMsg = Update.newBuilder()
                 .setEntity(Entity.newBuilder()
-                                   .setActionProfileGroup(actionProfileGroup)
+                                   .setActionProfileGroup(groupMsg)
                                    .build())
                 .setType(UPDATE_TYPES.get(opType))
                 .build();
@@ -961,7 +811,7 @@
                 .map(cellId -> PiMeterCellConfig.builder()
                         .withMeterCellId(cellId)
                         .build())
-                .collect(Collectors.toList());
+                .collect(toList());
 
         return doReadMeterEntities(MeterEntryCodec.encodePiMeterCellConfigs(
                 piMeterCellConfigs, pipeconf), pipeconf);
@@ -970,30 +820,9 @@
     private List<PiMeterCellConfig> doReadMeterEntities(
             List<Entity> entitiesToRead, PiPipeconf pipeconf) {
 
-        if (entitiesToRead.size() == 0) {
-            return Collections.emptyList();
-        }
-
-        final ReadRequest request = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addAllEntities(entitiesToRead)
-                .build();
-
-        final Iterable<ReadResponse> responses;
-        try {
-            responses = () -> blockingStub.read(request);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read meter cells: {}", e.getMessage());
-            log.debug("exception", e);
-            return Collections.emptyList();
-        }
-
-        List<Entity> responseEntities = StreamSupport
-                .stream(responses.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .collect(Collectors.toList());
+        final List<Entity> responseEntities = blockingRead(
+                entitiesToRead, METER_ENTRY, DIRECT_METER_ENTRY)
+                .collect(toList());
 
         return MeterEntryCodec.decodeMeterEntities(responseEntities, pipeconf);
     }
@@ -1007,7 +836,7 @@
                                      .setEntity(meterEntryMsg)
                                      .setType(UPDATE_TYPES.get(WriteOperationType.MODIFY))
                                      .build())
-                .collect(Collectors.toList());
+                .collect(toList());
 
         if (updateMsgs.size() == 0) {
             return true;
@@ -1024,7 +853,7 @@
                 .map(piEntry -> {
                     try {
                         return MulticastGroupEntryCodec.encode(piEntry);
-                    } catch (EncodeException e) {
+                    } catch (CodecException e) {
                         log.warn("Unable to encode PiMulticastGroupEntry: {}", e.getMessage());
                         return null;
                     }
@@ -1040,7 +869,7 @@
                         .setEntity(entityMsg)
                         .setType(UPDATE_TYPES.get(opType))
                         .build())
-                .collect(Collectors.toList());
+                .collect(toList());
         return write(updateMsgs, entries, opType, "multicast group entry");
     }
 
@@ -1055,32 +884,12 @@
                                 .build())
                 .build();
 
-        final ReadRequest req = ReadRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .addEntities(entity)
-                .build();
-
-        Iterator<ReadResponse> responses;
-        try {
-            responses = blockingStub.read(req);
-        } catch (StatusRuntimeException e) {
-            checkGrpcException(e);
-            log.warn("Unable to read multicast group entries from {}: {}", deviceId, e.getMessage());
-            return Collections.emptyList();
-        }
-
-        Iterable<ReadResponse> responseIterable = () -> responses;
-        final List<PiMulticastGroupEntry> mcEntries = StreamSupport
-                .stream(responseIterable.spliterator(), false)
-                .map(ReadResponse::getEntitiesList)
-                .flatMap(List::stream)
-                .filter(e -> e.getEntityCase()
-                        .equals(PACKET_REPLICATION_ENGINE_ENTRY))
+        final List<PiMulticastGroupEntry> mcEntries = blockingRead(entity, PACKET_REPLICATION_ENGINE_ENTRY)
                 .map(Entity::getPacketReplicationEngineEntry)
                 .filter(e -> e.getTypeCase().equals(MULTICAST_GROUP_ENTRY))
                 .map(PacketReplicationEngineEntry::getMulticastGroupEntry)
                 .map(MulticastGroupEntryCodec::decode)
-                .collect(Collectors.toList());
+                .collect(toList());
 
         log.debug("Retrieved {} multicast group entries from {}...",
                   mcEntries.size(), deviceId);
@@ -1126,6 +935,46 @@
                 .build();
     }
 
+    private Stream<Entity> blockingRead(Entity entity, Entity.EntityCase entityCase) {
+        return blockingRead(singletonList(entity), entityCase);
+    }
+
+    private Stream<Entity> blockingRead(Iterable<Entity> entities,
+                                        Entity.EntityCase... entityCases) {
+        // Build read request making sure we are reading what declared.
+        final ReadRequest.Builder reqBuilder = ReadRequest.newBuilder()
+                .setDeviceId(p4DeviceId);
+        final Set<Entity.EntityCase> entityCaseSet = Sets.newHashSet(entityCases);
+        for (Entity e : entities) {
+            checkArgument(entityCaseSet.contains(e.getEntityCase()),
+                          "Entity case mismatch");
+            reqBuilder.addEntities(e);
+        }
+        final ReadRequest readRequest = reqBuilder.build();
+        if (readRequest.getEntitiesCount() == 0) {
+            return Stream.empty();
+        }
+        // Issue read.
+        final Iterator<ReadResponse> responseIterator;
+        try {
+            responseIterator = blockingStub.read(readRequest);
+        } catch (StatusRuntimeException e) {
+            checkGrpcException(e);
+            final String caseString = entityCaseSet.stream()
+                    .map(Entity.EntityCase::name)
+                    .collect(joining("/"));
+            log.warn("Unable to read {} from {}: {}",
+                     caseString, deviceId, e.getMessage());
+            log.debug("Exception during read", e);
+            return Stream.empty();
+        }
+        // Filter results.
+        return Tools.stream(() -> responseIterator)
+                .map(ReadResponse::getEntitiesList)
+                .flatMap(List::stream)
+                .filter(e -> entityCaseSet.contains(e.getEntityCase()));
+    }
+
     protected Void doShutdown() {
         streamChannelManager.complete();
         return super.doShutdown();
@@ -1195,7 +1044,7 @@
                     }
                 })
                 .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+                .collect(toList());
     }
 
     private String parseP4Error(P4RuntimeOuterClass.Error err) {
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java
new file mode 100644
index 0000000..1126fef
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeCodecs.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.p4runtime.ctl;
+
+/**
+ * Utility class that provides access to P4Runtime codec instances.
+ */
+final class P4RuntimeCodecs {
+
+    static final P4RuntimeCodecs CODECS = new P4RuntimeCodecs();
+
+    private final ActionProfileMemberCodec actionProfileMember;
+    private final ActionProfileGroupCodec actionProfileGroup;
+
+    private P4RuntimeCodecs() {
+        this.actionProfileMember = new ActionProfileMemberCodec();
+        this.actionProfileGroup = new ActionProfileGroupCodec();
+    }
+
+    ActionProfileMemberCodec actionProfileMember() {
+        return actionProfileMember;
+    }
+
+    ActionProfileGroupCodec actionProfileGroup() {
+        return actionProfileGroup;
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
index 5dafe2b..604b1f0 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeUtils.java
@@ -31,20 +31,21 @@
     }
 
     static void assertSize(String entityDescr, ByteString value, int bitWidth)
-            throws EncodeException {
+            throws CodecException {
 
         int byteWidth = (int) Math.ceil((float) bitWidth / 8);
         if (value.size() != byteWidth) {
-            throw new EncodeException(format("Wrong size for %s, expected %d bytes, but found %d",
-                                             entityDescr, byteWidth, value.size()));
+            throw new CodecException(format(
+                    "Wrong size for %s, expected %d bytes, but found %d",
+                    entityDescr, byteWidth, value.size()));
         }
     }
 
     static void assertPrefixLen(String entityDescr, int prefixLength, int bitWidth)
-            throws EncodeException {
+            throws CodecException {
 
         if (prefixLength > bitWidth) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "wrong prefix length for %s, field size is %d bits, but found one is %d",
                     entityDescr, bitWidth, prefixLength));
         }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
index 3294a6d..357e41d 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
@@ -81,16 +81,16 @@
      * @param piTableEntries PI table entries
      * @param pipeconf       PI pipeconf
      * @return collection of P4Runtime table entry protobuf messages
-     * @throws EncodeException if a PI table entry cannot be encoded
+     * @throws CodecException if a PI table entry cannot be encoded
      */
     static List<TableEntry> encode(List<PiTableEntry> piTableEntries,
                                                 PiPipeconf pipeconf)
-            throws EncodeException {
+            throws CodecException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
 
         if (browser == null) {
-            throw new EncodeException(format(
+            throw new CodecException(format(
                     "Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
         }
 
@@ -100,7 +100,7 @@
             try {
                 tableEntryMsgListBuilder.add(encodePiTableEntry(piTableEntry, browser));
             } catch (P4InfoBrowser.NotFoundException e) {
-                throw new EncodeException(e.getMessage());
+                throw new CodecException(e.getMessage());
             }
         }
 
@@ -113,15 +113,15 @@
      * @param piTableEntry table entry
      * @param pipeconf     pipeconf
      * @return encoded table entry message
-     * @throws EncodeException                 if entry cannot be encoded
+     * @throws CodecException                 if entry cannot be encoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static TableEntry encode(PiTableEntry piTableEntry, PiPipeconf pipeconf)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            throw new EncodeException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
+            throw new CodecException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
         }
 
         return encodePiTableEntry(piTableEntry, browser);
@@ -152,7 +152,7 @@
         for (TableEntry tableEntryMsg : tableEntryMsgs) {
             try {
                 piTableEntryListBuilder.add(decodeTableEntryMsg(tableEntryMsg, browser));
-            } catch (P4InfoBrowser.NotFoundException | EncodeException e) {
+            } catch (P4InfoBrowser.NotFoundException | CodecException e) {
                 log.error("Unable to decode table entry message: {}", e.getMessage());
             }
         }
@@ -166,15 +166,15 @@
      * @param tableEntryMsg table entry message
      * @param pipeconf      pipeconf
      * @return decoded PI table entry
-     * @throws EncodeException                 if message cannot be decoded
+     * @throws CodecException                 if message cannot be decoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static PiTableEntry decode(TableEntry tableEntryMsg, PiPipeconf pipeconf)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         if (browser == null) {
-            throw new EncodeException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
+            throw new CodecException(format("Unable to get a P4Info browser for pipeconf %s", pipeconf.id()));
         }
         return decodeTableEntryMsg(tableEntryMsg, browser);
     }
@@ -188,11 +188,11 @@
      * @param matchKey match key
      * @param pipeconf pipeconf
      * @return table entry message
-     * @throws EncodeException                 if message cannot be encoded
+     * @throws CodecException                 if message cannot be encoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static TableEntry encode(PiTableId tableId, PiMatchKey matchKey, PiPipeconf pipeconf)
-            throws EncodeException, P4InfoBrowser.NotFoundException {
+            throws CodecException, P4InfoBrowser.NotFoundException {
 
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
@@ -215,7 +215,7 @@
     }
 
     private static TableEntry encodePiTableEntry(PiTableEntry piTableEntry, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         TableEntry.Builder tableEntryMsgBuilder = TableEntry.newBuilder();
 
@@ -259,7 +259,7 @@
     }
 
     private static PiTableEntry decodeTableEntryMsg(TableEntry tableEntryMsg, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         PiTableEntry.Builder piTableEntryBuilder = PiTableEntry.builder();
 
@@ -295,7 +295,7 @@
 
     private static FieldMatch encodePiFieldMatch(PiFieldMatch piFieldMatch, P4InfoOuterClass.Table tableInfo,
                                                  P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         FieldMatch.Builder fieldMatchMsgBuilder = FieldMatch.newBuilder();
 
@@ -359,7 +359,7 @@
                                 .build())
                         .build();
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Building of match type %s not implemented", piFieldMatch.type()));
         }
     }
@@ -370,11 +370,11 @@
      * @param tableEntryMsg table entry message
      * @param pipeconf      pipeconf
      * @return PI match key
-     * @throws EncodeException                 if message cannot be decoded
+     * @throws CodecException                 if message cannot be decoded
      * @throws P4InfoBrowser.NotFoundException if the required information cannot be find in the pipeconf's P4info
      */
     static PiMatchKey decodeMatchKey(TableEntry tableEntryMsg, PiPipeconf pipeconf)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
         P4InfoOuterClass.Table tableInfo = browser.tables().getById(tableEntryMsg.getTableId());
         if (tableEntryMsg.getMatchCount() == 0) {
@@ -386,7 +386,7 @@
 
     private static PiMatchKey decodeFieldMatchMsgs(List<FieldMatch> fieldMatchs, P4InfoOuterClass.Table tableInfo,
                                                    P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         // Match key for field matches.
         PiMatchKey.Builder piMatchKeyBuilder = PiMatchKey.builder();
         for (FieldMatch fieldMatchMsg : fieldMatchs) {
@@ -397,7 +397,7 @@
 
     private static PiFieldMatch decodeFieldMatchMsg(FieldMatch fieldMatchMsg, P4InfoOuterClass.Table tableInfo,
                                                     P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int tableId = tableInfo.getPreamble().getId();
         String fieldMatchName = browser.matchFields(tableId).getById(fieldMatchMsg.getFieldId()).getName();
@@ -426,13 +426,13 @@
                 ImmutableByteSequence rangeLowValue = copyFrom(rangeFieldMatch.getLow().asReadOnlyByteBuffer());
                 return new PiRangeFieldMatch(headerFieldId, rangeLowValue, rangeHighValue);
             default:
-                throw new EncodeException(format(
+                throw new CodecException(format(
                         "Decoding of field match type '%s' not implemented", typeCase.name()));
         }
     }
 
     static TableAction encodePiTableAction(PiTableAction piTableAction, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         checkNotNull(piTableAction, "Cannot encode null PiTableAction");
         TableAction.Builder tableActionMsgBuilder = TableAction.newBuilder();
 
@@ -451,7 +451,7 @@
                 tableActionMsgBuilder.setActionProfileMemberId(actionProfileMemberId.id());
                 break;
             default:
-                throw new EncodeException(
+                throw new CodecException(
                         format("Building of table action type %s not implemented", piTableAction.type()));
         }
 
@@ -459,7 +459,7 @@
     }
 
     static PiTableAction decodeTableActionMsg(TableAction tableActionMsg, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
         TableAction.TypeCase typeCase = tableActionMsg.getTypeCase();
         switch (typeCase) {
             case ACTION:
@@ -470,13 +470,13 @@
             case ACTION_PROFILE_MEMBER_ID:
                 return PiActionProfileMemberId.of(tableActionMsg.getActionProfileMemberId());
             default:
-                throw new EncodeException(
+                throw new CodecException(
                         format("Decoding of table action type %s not implemented", typeCase.name()));
         }
     }
 
     static Action encodePiAction(PiAction piAction, P4InfoBrowser browser)
-            throws P4InfoBrowser.NotFoundException, EncodeException {
+            throws P4InfoBrowser.NotFoundException, CodecException {
 
         int actionId = browser.actions().getByName(piAction.id().toString()).getPreamble().getId();
 
diff --git a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
index b51a2c3..a7f7183 100644
--- a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
+++ b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/P4RuntimeGroupTest.java
@@ -84,15 +84,19 @@
     private static final PiActionParamId PORT_PARAM_ID = PiActionParamId.of("port");
     private static final int BASE_MEM_ID = 65535;
     private static final List<Integer> MEMBER_IDS = ImmutableList.of(65536, 65537, 65538);
-    private static final List<PiActionProfileMember> GROUP_MEMBERS =
+    private static final List<PiActionProfileMember> GROUP_MEMBER_INSTANCES =
             Lists.newArrayList(
                     outputMember((short) 1),
                     outputMember((short) 2),
                     outputMember((short) 3)
             );
+    private static final List<PiActionProfileGroup.WeightedMember> GROUP_WEIGHTED_MEMBERS =
+            GROUP_MEMBER_INSTANCES.stream()
+                    .map(m -> new PiActionProfileGroup.WeightedMember(m, DEFAULT_MEMBER_WEIGHT))
+                    .collect(Collectors.toList());
     private static final PiActionProfileGroup GROUP = PiActionProfileGroup.builder()
             .withId(GROUP_ID)
-            .addMembers(GROUP_MEMBERS)
+            .addMembers(GROUP_MEMBER_INSTANCES)
             .withActionProfileId(ACT_PROF_ID)
             .build();
     private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:p4runtime:1");
@@ -121,7 +125,6 @@
                 .forActionProfile(ACT_PROF_ID)
                 .withAction(piAction)
                 .withId(PiActionProfileMemberId.of(BASE_MEM_ID + portNum))
-                .withWeight(DEFAULT_MEMBER_WEIGHT)
                 .build();
     }
 
@@ -163,7 +166,7 @@
     @Test
     public void testInsertPiActionProfileGroup() throws Exception {
         CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
-        client.writeActionProfileGroup(GROUP, INSERT, PIPECONF, 3);
+        client.writeActionProfileGroup(GROUP, INSERT, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
         WriteRequest result = p4RuntimeServerImpl.getWriteReqs().get(0);
         assertEquals(1, result.getDeviceId());
@@ -192,7 +195,7 @@
     @Test
     public void testInsertPiActionMembers() throws Exception {
         CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
-        client.writeActionProfileMembers(GROUP_MEMBERS, INSERT, PIPECONF);
+        client.writeActionProfileMembers(GROUP_MEMBER_INSTANCES, INSERT, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
         WriteRequest result = p4RuntimeServerImpl.getWriteReqs().get(0);
         assertEquals(1, result.getDeviceId());
@@ -224,15 +227,41 @@
                 .setGroupId(GROUP_ID.id())
                 .setActionProfileId(P4_INFO_ACT_PROF_ID);
 
-        List<ActionProfileMember> members = Lists.newArrayList();
-
         MEMBER_IDS.forEach(id -> {
             ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
                     .setMemberId(id)
                     .setWeight(DEFAULT_MEMBER_WEIGHT)
                     .build();
             group.addMembers(member);
+        });
 
+        List<ReadResponse> responses = Lists.newArrayList();
+        responses.add(ReadResponse.newBuilder()
+                              .addEntities(Entity.newBuilder().setActionProfileGroup(group))
+                              .build()
+        );
+
+        p4RuntimeServerImpl.willReturnReadResult(responses);
+        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
+        CompletableFuture<List<PiActionProfileGroup>> groupsComplete = client.dumpActionProfileGroups(
+                ACT_PROF_ID, PIPECONF);
+        complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
+
+        Collection<PiActionProfileGroup> groups = groupsComplete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
+        assertEquals(1, groups.size());
+        PiActionProfileGroup piActionGroup = groups.iterator().next();
+        assertEquals(ACT_PROF_ID, piActionGroup.actionProfile());
+        assertEquals(GROUP_ID, piActionGroup.id());
+        assertEquals(3, piActionGroup.members().size());
+        assertTrue(GROUP_WEIGHTED_MEMBERS.containsAll(piActionGroup.members()));
+        assertTrue(piActionGroup.members().containsAll(GROUP_WEIGHTED_MEMBERS));
+    }
+
+    @Test
+    public void testReadMembers() throws Exception {
+        List<ActionProfileMember> members = Lists.newArrayList();
+
+        MEMBER_IDS.forEach(id -> {
             byte outPort = (byte) (id - BASE_MEM_ID);
             ByteString bs = ByteString.copyFrom(new byte[]{0, outPort});
             Action.Param param = Action.Param.newBuilder()
@@ -256,11 +285,6 @@
 
         List<ReadResponse> responses = Lists.newArrayList();
         responses.add(ReadResponse.newBuilder()
-                              .addEntities(Entity.newBuilder().setActionProfileGroup(group))
-                              .build()
-        );
-
-        responses.add(ReadResponse.newBuilder()
                               .addAllEntities(members.stream()
                                                       .map(m -> Entity.newBuilder()
                                                               .setActionProfileMember(m).build())
@@ -268,18 +292,14 @@
                               .build());
 
         p4RuntimeServerImpl.willReturnReadResult(responses);
-        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(2);
-        CompletableFuture<List<PiActionProfileGroup>> groupsComplete = client.dumpActionProfileGroups(
+        CompletableFuture<Void> complete = p4RuntimeServerImpl.expectRequests(1);
+        CompletableFuture<List<PiActionProfileMember>> membersComplete = client.dumpActionProfileMembers(
                 ACT_PROF_ID, PIPECONF);
         complete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
 
-        Collection<PiActionProfileGroup> groups = groupsComplete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
-        assertEquals(1, groups.size());
-        PiActionProfileGroup piActionGroup = groups.iterator().next();
-        assertEquals(ACT_PROF_ID, piActionGroup.actionProfileId());
-        assertEquals(GROUP_ID, piActionGroup.id());
-        assertEquals(3, piActionGroup.members().size());
-        assertTrue(GROUP_MEMBERS.containsAll(piActionGroup.members()));
-        assertTrue(piActionGroup.members().containsAll(GROUP_MEMBERS));
+        Collection<PiActionProfileMember> piMembers = membersComplete.get(DEFAULT_TIMEOUT_TIME, TimeUnit.SECONDS);
+        assertEquals(3, piMembers.size());
+        assertTrue(GROUP_MEMBER_INSTANCES.containsAll(piMembers));
+        assertTrue(piMembers.containsAll(GROUP_MEMBER_INSTANCES));
     }
 }
diff --git a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java
index 5417d44..443d407 100644
--- a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java
+++ b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4ActionProfileModel.java
@@ -32,14 +32,17 @@
     private final PiActionProfileId id;
     private final ImmutableSet<PiTableId> tables;
     private final boolean hasSelector;
-    private final long maxSize;
+    private final long size;
+    private final int maxGroupSize;
 
     P4ActionProfileModel(PiActionProfileId id,
-                         ImmutableSet<PiTableId> tables, boolean hasSelector, long maxSize) {
+                         ImmutableSet<PiTableId> tables, boolean hasSelector,
+                         long size, int maxGroupSize) {
         this.id = id;
         this.tables = tables;
         this.hasSelector = hasSelector;
-        this.maxSize = maxSize;
+        this.size = size;
+        this.maxGroupSize = maxGroupSize;
     }
 
     @Override
@@ -58,13 +61,18 @@
     }
 
     @Override
-    public long maxSize() {
-        return maxSize;
+    public long size() {
+        return size;
+    }
+
+    @Override
+    public int maxGroupSize() {
+        return maxGroupSize;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(id, tables, hasSelector, maxSize);
+        return Objects.hash(id, tables, hasSelector, size, maxGroupSize);
     }
 
     @Override
@@ -79,6 +87,7 @@
         return Objects.equals(this.id, other.id)
                 && Objects.equals(this.tables, other.tables)
                 && Objects.equals(this.hasSelector, other.hasSelector)
-                && Objects.equals(this.maxSize, other.maxSize);
+                && Objects.equals(this.size, other.size)
+                && Objects.equals(this.maxGroupSize, other.maxGroupSize);
     }
 }
diff --git a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java
index ff49928..ef8cb36 100644
--- a/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java
+++ b/protocols/p4runtime/model/src/main/java/org/onosproject/p4runtime/model/P4InfoParser.java
@@ -332,7 +332,8 @@
                             PiActionProfileId.of(actProfileMsg.getPreamble().getName()),
                             tableIdSetBuilder.build(),
                             actProfileMsg.getWithSelector(),
-                            actProfileMsg.getSize()));
+                            actProfileMsg.getSize(),
+                            actProfileMsg.getMaxGroupSize()));
         }
         return actProfileMap;
     }
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java
index 3042414..021d341 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4ActionProfileModelTest.java
@@ -21,7 +21,6 @@
 import org.onosproject.net.pi.model.PiActionProfileId;
 import org.onosproject.net.pi.model.PiTableId;
 
-
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 
 /**
@@ -52,17 +51,17 @@
     private final PiActionProfileId id2 = PiActionProfileId.of("name2");
 
     private final P4ActionProfileModel metadataModel = new P4ActionProfileModel(id, tables,
-                                                                                true, 64);
+                                                                                true, 64, 10);
     private final P4ActionProfileModel sameAsMetadataModel = new P4ActionProfileModel(id, sameAsTables,
-                                                                                      true, 64);
+                                                                                      true, 64, 10);
     private final P4ActionProfileModel metadataModel2 = new P4ActionProfileModel(id, tables2,
-                                                                                 true, 64);
+                                                                                 true, 64, 10);
     private final P4ActionProfileModel metadataModel3 = new P4ActionProfileModel(id2, tables,
-                                                                                 true, 64);
+                                                                                 true, 64, 10);
     private final P4ActionProfileModel metadataModel4 = new P4ActionProfileModel(id, tables,
-                                                                                 false, 64);
+                                                                                 false, 64, 10);
     private final P4ActionProfileModel metadataModel5 = new P4ActionProfileModel(id, tables,
-                                                                                 true, 32);
+                                                                                 true, 32, 5);
 
     /**
      * Checks that the P4ActionProfileModel class is immutable.
@@ -85,4 +84,4 @@
                 .addEqualityGroup(metadataModel5)
                 .testEquals();
     }
-}
\ No newline at end of file
+}
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java
index 3a803d4..9d696e2 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4InfoParserTest.java
@@ -70,6 +70,7 @@
 
     private static final Long DEFAULT_MAX_TABLE_SIZE = 1024L;
     private static final Long DEFAULT_MAX_ACTION_PROFILE_SIZE = 64L;
+    private static final int DEFAULT_MAX_GROUP_SIZE = 0;
 
     /**
      * Tests parse method.
@@ -205,7 +206,8 @@
         ImmutableSet<PiTableId> tableIds = new ImmutableSet.Builder<PiTableId>().add(tableId).build();
         PiActionProfileId actionProfileId = PiActionProfileId.of("wcmp_control.wcmp_selector");
         PiActionProfileModel wcmpSelector3 = new P4ActionProfileModel(actionProfileId, tableIds,
-                                                                      true, DEFAULT_MAX_ACTION_PROFILE_SIZE);
+                                                                      true, DEFAULT_MAX_ACTION_PROFILE_SIZE,
+                                                                      DEFAULT_MAX_GROUP_SIZE);
         PiActionProfileModel wcmpSelector = model.actionProfiles(actionProfileId).orElse(null);
         PiActionProfileModel wcmpSelector2 = model2.actionProfiles(actionProfileId).orElse(null);
 
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java
index da10762..8576e84 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4PipelineModelTest.java
@@ -75,12 +75,17 @@
     private static final long ACTION_MAX_SIZE_1 = 100;
     private static final long ACTION_MAX_SIZE_2 = 200;
 
+    private static final int ACTION_MAX_GROUP_SIZE_1 = 10;
+    private static final int ACTION_MAX_GROUP_SIZE_2 = 20;
+
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_1 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_1, ACTION_TABLES_1,
-                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1);
+                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1,
+                                     ACTION_MAX_GROUP_SIZE_1);
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_2 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_2, ACTION_TABLES_2,
-                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2);
+                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2,
+                                     ACTION_MAX_GROUP_SIZE_2);
 
     /* Counters */
     private static final PiCounterId PI_COUNTER_ID_1 = PiCounterId.of("Counter1");
diff --git a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java
index 89b10d0..797abe4 100644
--- a/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java
+++ b/protocols/p4runtime/model/src/test/java/org/onosproject/p4runtime/model/P4TableModelTest.java
@@ -67,12 +67,17 @@
     private static final long ACTION_MAX_SIZE_1 = 100;
     private static final long ACTION_MAX_SIZE_2 = 200;
 
+    private static final int ACTION_MAX_GROUP_SIZE_1 = 10;
+    private static final int ACTION_MAX_GROUP_SIZE_2 = 20;
+
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_1 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_1, ACTION_TABLES_1,
-                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1);
+                                     ACTION_HAS_SELECTOR_1, ACTION_MAX_SIZE_1,
+                                     ACTION_MAX_GROUP_SIZE_1);
     private static final PiActionProfileModel P4_ACTION_PROFILE_MODEL_2 =
             new P4ActionProfileModel(PI_ACTION_PROFILE_ID_2, ACTION_TABLES_2,
-                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2);
+                                     ACTION_HAS_SELECTOR_2, ACTION_MAX_SIZE_2,
+                                     ACTION_MAX_GROUP_SIZE_2);
 
     /* Counters */
     private static final PiCounterId PI_COUNTER_ID_1 = PiCounterId.of("Counter1");