Separate out `AttributePathExpandIterator::Position` (#36980)

* Copied over the new AttributePathExpandIterator and will incrementally use it (so I can validate tests)

* Rename AttributePathExpandIterator to legacy

* Prepare for using new style iterators ... checking NOT YET enabled though

* Enabled checks ... and unit tests fail, but this now can be debugged

* Fix some of the underlying bugs: read handling logic assumes we are ok to undo

* Unit tests pass now

* Restyle

* Use new iterator in IME

* Update logic to use the new iterator on testRead

* more updates

* Restyle

* Remove the legacy attribute path expand iterator

* Update naming

* Restyle

* Remove extra argument for ReadHandler constructor

* Restyle

* Slight flash improvement

* Fix up includes

* Removed empty line

* added comment on why state is a friend class

* Comment updates

* Restyle, add some comments and add extra checks on validity check only for expansion. This saves a tiny amount of flash (32 bytes)

* Remove an include

* Comment updates, renamed mLastOutputPath to mOutputPath

* Fix one typo

* Re-arrange members of ReadHandler to optimize for memory layout. This saves 8 bytes for struct. We still have a 20-byte padding which I am unsure how to get rid of

* Restyle

* Rename State to Position

* One more rename

* Remove redundant assigment ...we are at a net 0 txt increase now on qpg

* Add more unit tests for non-obvious requirement that wildcard expansion checks path validity, however non-wildcard does not check it

* Update src/app/AttributePathExpandIterator.cpp

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Update src/app/AttributePathExpandIterator.h

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Update src/app/AttributePathExpandIterator.h

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Update src/app/AttributePathExpandIterator.h

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Update src/app/ReadHandler.h

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Update src/app/ReadHandler.cpp

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Update src/app/AttributePathExpandIterator.h

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* Use different values for the cluster ids for testing

* One more state to position change

* mExpanded is now set during output path returning. Removed 2 more sets to save another tinier amount of .text

* Remove some tests that seem redundant, keep only one

* Update src/app/AttributePathExpandIterator.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/AttributePathExpandIterator.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/AttributePathExpandIterator.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/AttributePathExpandIterator.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/InteractionModelEngine.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/ReadHandler.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/AttributePathExpandIterator.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/ReadHandler.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Use mCompletePosition

* Another rename

* Undo submodule update

* Restyle

* Update comment text to not sound like graph parsing

* Rename method to be more descriptive

* Update peek attribute iterator to rollback and update code logic a bit. Hoping for cleaner code

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/src/app/AttributePathExpandIterator.cpp b/src/app/AttributePathExpandIterator.cpp
index 419067d..23273c7 100644
--- a/src/app/AttributePathExpandIterator.cpp
+++ b/src/app/AttributePathExpandIterator.cpp
@@ -19,22 +19,86 @@
 #include <app/GlobalAttributes.h>
 #include <lib/support/CodeUtils.h>
 
+#include <optional>
+
 using namespace chip::app::DataModel;
 
 namespace chip {
 namespace app {
 
-AttributePathExpandIterator::AttributePathExpandIterator(DataModel::Provider * provider,
-                                                         SingleLinkedListNode<AttributePathParams> * attributePath) :
-    mDataModelProvider(provider),
-    mpAttributePath(attributePath), mOutputPath(kInvalidEndpointId, kInvalidClusterId, kInvalidAttributeId)
-
+bool AttributePathExpandIterator::AdvanceOutputPath()
 {
-    mOutputPath.mExpanded = true; // this is reset in 'next' if needed
+    /// Output path invariants
+    ///    - kInvalid* constants are used to define "no value available (yet)" and
+    ///      iteration loop will fill the first value when such a value is seen (fixed for non-wildcard
+    ///      or iteration-based in case of wildcards).
+    ///    - Iteration of the output path is done in order: first endpoint, then cluster, then attribute.
+    /// Processing works like:
+    ///    - Initial state is kInvalidEndpointId/kInvalidClusterId/kInvalidAttributeId
+    ///    - First loop pass fills-in endpointID, followed by clusterID, followed by attributeID
+    ///    - Whenever one level is done iterating (there is no "next") the following
+    ///      "higher path component" is updated:
+    ///         - once a valid path exists, try to advance attributeID
+    ///         - if attributeID fails to advance, try to advance clusterID (and restart attributeID)
+    ///         - if clusterID fails to advance, try to advance endpointID (and restart clusterID)
+    ///         - if endpointID fails to advance, iteration is done
+    while (true)
+    {
+        if (mPosition.mOutputPath.mClusterId != kInvalidClusterId)
+        {
+            std::optional<AttributeId> nextAttribute = NextAttributeId();
+            if (nextAttribute.has_value())
+            {
+                mPosition.mOutputPath.mAttributeId = *nextAttribute;
+                mPosition.mOutputPath.mExpanded    = mPosition.mAttributePath->mValue.IsWildcardPath();
+                return true;
+            }
+        }
 
-    // Make the iterator ready to emit the first valid path in the list.
-    // TODO: the bool return value here is completely unchecked
-    Next();
+        // no valid attribute, try to advance the cluster, see if a suitable one exists
+        if (mPosition.mOutputPath.mEndpointId != kInvalidEndpointId)
+        {
+            std::optional<ClusterId> nextCluster = NextClusterId();
+            if (nextCluster.has_value())
+            {
+                // A new cluster ID is to be processed. This sets the cluster ID to the new value and
+                // ALSO resets the attribute ID to "invalid", to trigger an attribute set/expansion from
+                // the beginning.
+                mPosition.mOutputPath.mClusterId   = *nextCluster;
+                mPosition.mOutputPath.mAttributeId = kInvalidAttributeId;
+                continue;
+            }
+        }
+
+        // No valid cluster, try advance the endpoint, see if a suitable one exists.
+        std::optional<EndpointId> nextEndpoint = NextEndpointId();
+        if (nextEndpoint.has_value())
+        {
+            // A new endpoint ID is to be processed. This sets the endpoint ID to the new value and
+            // ALSO resets the cluster ID to "invalid", to trigger a cluster set/expansion from
+            // the beginning.
+            mPosition.mOutputPath.mEndpointId = *nextEndpoint;
+            mPosition.mOutputPath.mClusterId  = kInvalidClusterId;
+            continue;
+        }
+        return false;
+    }
+}
+
+bool AttributePathExpandIterator::Next(ConcreteAttributePath & path)
+{
+    while (mPosition.mAttributePath != nullptr)
+    {
+        if (AdvanceOutputPath())
+        {
+            path = mPosition.mOutputPath;
+            return true;
+        }
+        mPosition.mAttributePath = mPosition.mAttributePath->mpNext;
+        mPosition.mOutputPath    = ConcreteReadAttributePath(kInvalidEndpointId, kInvalidClusterId, kInvalidAttributeId);
+    }
+
+    return false;
 }
 
 bool AttributePathExpandIterator::IsValidAttributeId(AttributeId attributeId)
@@ -49,40 +113,43 @@
         break;
     }
 
-    const ConcreteAttributePath attributePath(mOutputPath.mEndpointId, mOutputPath.mClusterId, attributeId);
+    const ConcreteAttributePath attributePath(mPosition.mOutputPath.mEndpointId, mPosition.mOutputPath.mClusterId, attributeId);
     return mDataModelProvider->GetAttributeInfo(attributePath).has_value();
 }
 
 std::optional<AttributeId> AttributePathExpandIterator::NextAttributeId()
 {
-    if (mOutputPath.mAttributeId == kInvalidAttributeId)
+    if (mPosition.mOutputPath.mAttributeId == kInvalidAttributeId)
     {
-        if (mpAttributePath->mValue.HasWildcardAttributeId())
+        if (mPosition.mAttributePath->mValue.HasWildcardAttributeId())
         {
-            AttributeEntry entry = mDataModelProvider->FirstAttribute(mOutputPath);
+            AttributeEntry entry = mDataModelProvider->FirstAttribute(mPosition.mOutputPath);
             return entry.IsValid()                                         //
                 ? entry.path.mAttributeId                                  //
                 : Clusters::Globals::Attributes::GeneratedCommandList::Id; //
         }
 
-        // We allow fixed attribute IDs if and only if they are valid:
-        //    - they may be GLOBAL attributes OR
-        //    - they are valid attributes for this cluster
-        if (IsValidAttributeId(mpAttributePath->mValue.mAttributeId))
+        // At this point, the attributeID is NOT a wildcard (i.e. it is fixed).
+        //
+        // For wildcard expansion, we validate that this is a valid attribute for the given
+        // cluster on the given endpoint. If not a wildcard expansion, return it as-is.
+        if (mPosition.mAttributePath->mValue.IsWildcardPath())
         {
-            return mpAttributePath->mValue.mAttributeId;
+            if (!IsValidAttributeId(mPosition.mAttributePath->mValue.mAttributeId))
+            {
+                return std::nullopt;
+            }
         }
-
-        return std::nullopt;
+        return mPosition.mAttributePath->mValue.mAttributeId;
     }
 
-    // advance the existing attribute id if it can be advanced
-    VerifyOrReturnValue(mpAttributePath->mValue.HasWildcardAttributeId(), std::nullopt);
+    // Advance the existing attribute id if it can be advanced.
+    VerifyOrReturnValue(mPosition.mAttributePath->mValue.HasWildcardAttributeId(), std::nullopt);
 
     // Ensure (including ordering) that GlobalAttributesNotInMetadata is reported as needed
     for (unsigned i = 0; i < ArraySize(GlobalAttributesNotInMetadata); i++)
     {
-        if (GlobalAttributesNotInMetadata[i] != mOutputPath.mAttributeId)
+        if (GlobalAttributesNotInMetadata[i] != mPosition.mOutputPath.mAttributeId)
         {
             continue;
         }
@@ -93,11 +160,12 @@
             return GlobalAttributesNotInMetadata[nextAttributeIndex];
         }
 
-        // reached the end of global attributes
+        // Reached the end of global attributes. Since global attributes are
+        // reported last, finishing global attributes means everything completed.
         return std::nullopt;
     }
 
-    AttributeEntry entry = mDataModelProvider->NextAttribute(mOutputPath);
+    AttributeEntry entry = mDataModelProvider->NextAttribute(mPosition.mOutputPath);
     if (entry.IsValid())
     {
         return entry.path.mAttributeId;
@@ -111,130 +179,55 @@
 std::optional<ClusterId> AttributePathExpandIterator::NextClusterId()
 {
 
-    if (mOutputPath.mClusterId == kInvalidClusterId)
+    if (mPosition.mOutputPath.mClusterId == kInvalidClusterId)
     {
-        if (mpAttributePath->mValue.HasWildcardClusterId())
+        if (mPosition.mAttributePath->mValue.HasWildcardClusterId())
         {
-            ClusterEntry entry = mDataModelProvider->FirstServerCluster(mOutputPath.mEndpointId);
+            ClusterEntry entry = mDataModelProvider->FirstServerCluster(mPosition.mOutputPath.mEndpointId);
             return entry.IsValid() ? std::make_optional(entry.path.mClusterId) : std::nullopt;
         }
 
-        // only return a cluster if it is valid
-        const ConcreteClusterPath clusterPath(mOutputPath.mEndpointId, mpAttributePath->mValue.mClusterId);
-        if (!mDataModelProvider->GetServerClusterInfo(clusterPath).has_value())
+        // At this point, the clusterID is NOT a wildcard (i.e. is fixed).
+        //
+        // For wildcard expansion, we validate that this is a valid cluster for the endpoint.
+        // If non-wildcard expansion, we return as-is.
+        if (mPosition.mAttributePath->mValue.IsWildcardPath())
         {
-            return std::nullopt;
+            const ConcreteClusterPath clusterPath(mPosition.mOutputPath.mEndpointId, mPosition.mAttributePath->mValue.mClusterId);
+            if (!mDataModelProvider->GetServerClusterInfo(clusterPath).has_value())
+            {
+                return std::nullopt;
+            }
         }
 
-        return mpAttributePath->mValue.mClusterId;
+        return mPosition.mAttributePath->mValue.mClusterId;
     }
 
-    VerifyOrReturnValue(mpAttributePath->mValue.HasWildcardClusterId(), std::nullopt);
+    VerifyOrReturnValue(mPosition.mAttributePath->mValue.HasWildcardClusterId(), std::nullopt);
 
-    ClusterEntry entry = mDataModelProvider->NextServerCluster(mOutputPath);
+    ClusterEntry entry = mDataModelProvider->NextServerCluster(mPosition.mOutputPath);
     return entry.IsValid() ? std::make_optional(entry.path.mClusterId) : std::nullopt;
 }
 
 std::optional<ClusterId> AttributePathExpandIterator::NextEndpointId()
 {
-    if (mOutputPath.mEndpointId == kInvalidEndpointId)
+    if (mPosition.mOutputPath.mEndpointId == kInvalidEndpointId)
     {
-        if (mpAttributePath->mValue.HasWildcardEndpointId())
+        if (mPosition.mAttributePath->mValue.HasWildcardEndpointId())
         {
             EndpointEntry ep = mDataModelProvider->FirstEndpoint();
             return (ep.id != kInvalidEndpointId) ? std::make_optional(ep.id) : std::nullopt;
         }
 
-        return mpAttributePath->mValue.mEndpointId;
+        return mPosition.mAttributePath->mValue.mEndpointId;
     }
 
-    VerifyOrReturnValue(mpAttributePath->mValue.HasWildcardEndpointId(), std::nullopt);
+    // Expand endpoints only if it is a wildcard on the endpoint specifically.
+    VerifyOrReturnValue(mPosition.mAttributePath->mValue.HasWildcardEndpointId(), std::nullopt);
 
-    EndpointEntry ep = mDataModelProvider->NextEndpoint(mOutputPath.mEndpointId);
+    EndpointEntry ep = mDataModelProvider->NextEndpoint(mPosition.mOutputPath.mEndpointId);
     return (ep.id != kInvalidEndpointId) ? std::make_optional(ep.id) : std::nullopt;
 }
 
-void AttributePathExpandIterator::ResetCurrentCluster()
-{
-    // If this is a null iterator, or the attribute id of current cluster info is not a wildcard attribute id, then this function
-    // will do nothing, since we won't be expanding the wildcard attribute ids under a cluster.
-    VerifyOrReturn(mpAttributePath != nullptr && mpAttributePath->mValue.HasWildcardAttributeId());
-
-    // Reset path expansion to ask for the first attribute of the current cluster
-    mOutputPath.mAttributeId = kInvalidAttributeId;
-    mOutputPath.mExpanded    = true; // we know this is a wildcard attribute
-    Next();
-}
-
-bool AttributePathExpandIterator::AdvanceOutputPath()
-{
-    if (!mpAttributePath->mValue.IsWildcardPath())
-    {
-        if (mOutputPath.mEndpointId != kInvalidEndpointId)
-        {
-            return false; // cannot expand non-wildcard path
-        }
-
-        mOutputPath.mEndpointId  = mpAttributePath->mValue.mEndpointId;
-        mOutputPath.mClusterId   = mpAttributePath->mValue.mClusterId;
-        mOutputPath.mAttributeId = mpAttributePath->mValue.mAttributeId;
-        mOutputPath.mExpanded    = false;
-        return true;
-    }
-
-    while (true)
-    {
-        if (mOutputPath.mClusterId != kInvalidClusterId)
-        {
-
-            std::optional<AttributeId> nextAttribute = NextAttributeId();
-            if (nextAttribute.has_value())
-            {
-                mOutputPath.mAttributeId = *nextAttribute;
-                return true;
-            }
-        }
-
-        // no valid attribute, try to advance the cluster, see if a suitable one exists
-        if (mOutputPath.mEndpointId != kInvalidEndpointId)
-        {
-            std::optional<ClusterId> nextCluster = NextClusterId();
-            if (nextCluster.has_value())
-            {
-                mOutputPath.mClusterId   = *nextCluster;
-                mOutputPath.mAttributeId = kInvalidAttributeId; // restarts attributes
-                continue;
-            }
-        }
-
-        // no valid cluster, try advance the endpoint, see if a suitable on exists
-        std::optional<EndpointId> nextEndpoint = NextEndpointId();
-        if (nextEndpoint.has_value())
-        {
-            mOutputPath.mEndpointId = *nextEndpoint;
-            mOutputPath.mClusterId  = kInvalidClusterId; // restarts clusters
-            continue;
-        }
-        return false;
-    }
-}
-
-bool AttributePathExpandIterator::Next()
-{
-    while (mpAttributePath != nullptr)
-    {
-        if (AdvanceOutputPath())
-        {
-            return true;
-        }
-        mpAttributePath       = mpAttributePath->mpNext;
-        mOutputPath           = ConcreteReadAttributePath(kInvalidEndpointId, kInvalidClusterId, kInvalidAttributeId);
-        mOutputPath.mExpanded = true; // this is reset to false on advancement if needed
-    }
-
-    mOutputPath = ConcreteReadAttributePath();
-    return false;
-}
-
 } // namespace app
 } // namespace chip
diff --git a/src/app/AttributePathExpandIterator.h b/src/app/AttributePathExpandIterator.h
index 351520f..2916a2e 100644
--- a/src/app/AttributePathExpandIterator.h
+++ b/src/app/AttributePathExpandIterator.h
@@ -20,81 +20,104 @@
 #include <app/AttributePathParams.h>
 #include <app/ConcreteAttributePath.h>
 #include <app/data-model-provider/Provider.h>
+#include <lib/core/DataModelTypes.h>
 #include <lib/support/LinkedList.h>
 
 namespace chip {
 namespace app {
 
-/**
- * AttributePathExpandIterator is used to iterate over a linked list of AttributePathParams-s.
- * The AttributePathExpandIterator is copiable, however, the given cluster info must be valid when calling Next().
- *
- * AttributePathExpandIterator will expand attribute paths with wildcards, and only emit existing paths for
- * AttributePathParams with wildcards. For AttributePathParams with a concrete path (i.e. does not contain wildcards),
- * AttributePathExpandIterator will emit them as-is.
- *
- * The typical use of AttributePathExpandIterator may look like:
- * ConcreteAttributePath path;
- * for (AttributePathExpandIterator iterator(AttributePathParams); iterator.Get(path); iterator.Next()) {...}
- *
- * The iterator does not copy the given AttributePathParams. The given AttributePathParams must remain valid when using the
- * iterator. If the set of endpoints, clusters, or attributes that are supported changes, AttributePathExpandIterator must be
- * reinitialized.
- *
- * A initialized iterator will return the first valid path, no need to call Next() before calling Get() for the first time.
- *
- * Note: Next() and Get() are two separate operations by design since a possible call of this iterator might be:
- * - Get()
- * - Chunk full, return
- * - In a new chunk, Get()
- *
- * TODO: The AttributePathParams may support a group id, the iterator should be able to call group data provider to expand the group
- * id.
- */
+/// Handles attribute path expansions
+/// Usage:
+///
+/// - Start iterating by creating an iteration state
+///
+///      AttributePathExpandIterator::Position position = AttributePathExpandIterator::Position::StartIterating(path);
+///
+/// - Use the iteration state in a for loop:
+///
+///      ConcreteAttributePath path;
+///      for (AttributePathExpandIterator iterator(position); iterator->Next(path);) {
+///         // use `path` here`
+///      }
+///
+///   OR:
+///
+///      ConcreteAttributePath path;
+///      AttributePathExpandIterator iterator(position);
+///
+///      while (iterator.Next(path)) {
+///         // use `path` here`
+///      }
+///
+/// Usage requirements and assumptions:
+///
+///    - An ` AttributePathExpandIterator::Position` can only be used by a single AttributePathExpandIterator at a time.
+///
+///    - `position` is automatically updated by the AttributePathExpandIterator, so
+///      calling `Next` on the iterator will update the position cursor variable.
+///
 class AttributePathExpandIterator
 {
 public:
-    AttributePathExpandIterator(DataModel::Provider * provider, SingleLinkedListNode<AttributePathParams> * attributePath);
-
-    /**
-     * Proceed the iterator to the next attribute path in the given cluster info.
-     *
-     * Returns false if AttributePathExpandIteratorDataModeDataModel has exhausted all paths in the given AttributePathParams list.
-     */
-    bool Next();
-
-    /**
-     * Fills the aPath with the path the iterator currently points to.
-     * Returns false if the iterator is not pointing to a valid path (i.e. it has exhausted the cluster info).
-     */
-    bool Get(ConcreteAttributePath & aPath)
+    class Position
     {
-        aPath = mOutputPath;
-        return (mpAttributePath != nullptr);
-    }
+    public:
+        // Position is treated as a direct member access by the AttributePathExpandIterator, however it is opaque (except copying)
+        // for external code. We allow friendship here to not have specific get/set for methods (clearer interface and less
+        // likelihood of extra code usage).
+        friend class AttributePathExpandIterator;
 
-    /**
-     * Reset the iterator to the beginning of current cluster if we are in the middle of expanding a wildcard attribute id for some
-     * cluster.
-     *
-     * When attributes are changed in the middle of expanding a wildcard attribute, we need to reset the iterator, to provide the
-     * client with a consistent state of the cluster.
-     */
-    void ResetCurrentCluster();
+        /// External callers can only ever start iterating on a new path from the beginning
+        static Position StartIterating(SingleLinkedListNode<AttributePathParams> * path) { return Position(path); }
 
-    /** Start iterating over the given `paths` */
-    inline void ResetTo(SingleLinkedListNode<AttributePathParams> * paths)
-    {
-        *this = AttributePathExpandIterator(mDataModelProvider, paths);
-    }
+        /// Copies are allowed
+        Position(const Position &)             = default;
+        Position & operator=(const Position &) = default;
+
+        Position() : mAttributePath(nullptr) {}
+
+        /// Reset the iterator to the beginning of current cluster if we are in the middle of expanding a wildcard attribute id for
+        /// some cluster.
+        ///
+        /// When attributes are changed in the middle of expanding a wildcard attribute, we need to reset the iterator, to provide
+        /// the client with a consistent state of the cluster.
+        void IterateFromTheStartOfTheCurrentClusterIfAttributeWildcard()
+        {
+            VerifyOrReturn(mAttributePath != nullptr && mAttributePath->mValue.HasWildcardAttributeId());
+            mOutputPath.mAttributeId = kInvalidAttributeId;
+        }
+
+    protected:
+        Position(SingleLinkedListNode<AttributePathParams> * path) :
+            mAttributePath(path), mOutputPath(kInvalidEndpointId, kInvalidClusterId, kInvalidAttributeId)
+        {}
+
+        SingleLinkedListNode<AttributePathParams> * mAttributePath;
+        ConcreteAttributePath mOutputPath;
+    };
+
+    AttributePathExpandIterator(DataModel::Provider * dataModel, Position & position) :
+        mDataModelProvider(dataModel), mPosition(position)
+    {}
+
+    // This class may not be copied. A new one should be created when needed and they
+    // should not overlap.
+    AttributePathExpandIterator(const AttributePathExpandIterator &)             = delete;
+    AttributePathExpandIterator & operator=(const AttributePathExpandIterator &) = delete;
+
+    /// Get the next path of the expansion (if one exists).
+    ///
+    /// On success, true is returned and `path` is filled with the next path in the
+    /// expansion.
+    /// On iteration completion, false is returned and the content of path IS NOT DEFINED.
+    bool Next(ConcreteAttributePath & path);
 
 private:
     DataModel::Provider * mDataModelProvider;
-    SingleLinkedListNode<AttributePathParams> * mpAttributePath;
-    ConcreteAttributePath mOutputPath;
+    Position & mPosition;
 
     /// Move to the next endpoint/cluster/attribute triplet that is valid given
-    /// the current mOutputPath and mpAttributePath
+    /// the current mOutputPath and mpAttributePath.
     ///
     /// returns true if such a next value was found.
     bool AdvanceOutputPath();
@@ -125,5 +148,52 @@
     bool IsValidAttributeId(AttributeId attributeId);
 };
 
+/// RollbackAttributePathExpandIterator is an AttributePathExpandIterator wrapper that rolls back the Next()
+/// call whenever a new `MarkCompleted()` method is not called.
+///
+/// Example use cases:
+///
+/// - Iterate over all attributes and process one-by-one, however when the iteration fails, resume at
+///   the last failure point:
+///
+///      RollbackAttributePathExpandIterator iterator(....);
+///      ConcreteAttributePath path;
+///
+///      for ( ; iterator.Next(path); iterator.MarkCompleted()) {
+///         if (!CanProcess(path)) {
+///             // iterator state IS PRESERVED so that Next() will return the SAME path on the next call.
+///             return CHIP_ERROR_TRY_AGAIN_LATER;
+///         }
+///      }
+///
+/// -  Grab what the next output path would be WITHOUT advancing a state;
+///
+///      {
+///        RollbackAttributePathExpandIterator iterator(...., state);
+///        if (iterator.Next(...)) { ... }
+///      }
+///      // state here is ROLLED BACK (i.e. initializing a new iterator with it will start at the same place as the previous
+///      iteration attempt).
+///
+///
+class RollbackAttributePathExpandIterator
+{
+public:
+    RollbackAttributePathExpandIterator(DataModel::Provider * dataModel, AttributePathExpandIterator::Position & position) :
+        mAttributePathExpandIterator(dataModel, position), mPositionTarget(position), mCompletedPosition(position)
+    {}
+    ~RollbackAttributePathExpandIterator() { mPositionTarget = mCompletedPosition; }
+
+    bool Next(ConcreteAttributePath & path) { return mAttributePathExpandIterator.Next(path); }
+
+    /// Marks the current iteration completed (so peek does not actually roll back)
+    void MarkCompleted() { mCompletedPosition = mPositionTarget; }
+
+private:
+    AttributePathExpandIterator mAttributePathExpandIterator;
+    AttributePathExpandIterator::Position & mPositionTarget;
+    AttributePathExpandIterator::Position mCompletedPosition;
+};
+
 } // namespace app
 } // namespace chip
diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn
index 1ee02cd..3fb0795 100644
--- a/src/app/BUILD.gn
+++ b/src/app/BUILD.gn
@@ -207,6 +207,7 @@
   ]
 
   deps = [
+    ":path-expansion",
     "${chip_root}/src/app:events",
     "${chip_root}/src/app:global-attributes",
   ]
@@ -406,6 +407,21 @@
   ]
 }
 
+source_set("path-expansion") {
+  sources = [
+    "AttributePathExpandIterator.cpp",
+    "AttributePathExpandIterator.h",
+  ]
+
+  public_deps = [
+    ":global-attributes",
+    ":paths",
+    "${chip_root}/src/app/data-model-provider",
+    "${chip_root}/src/lib/core:types",
+    "${chip_root}/src/lib/support",
+  ]
+}
+
 source_set("attribute-persistence") {
   sources = [
     "DefaultSafeAttributePersistenceProvider.h",
@@ -429,9 +445,6 @@
   output_name = "libCHIPDataModel"
 
   sources = [
-    "AttributePathExpandIterator.cpp",
-    "AttributePathExpandIterator.h",
-    "AttributePathExpandIterator.h",
     "ChunkedWriteCallback.cpp",
     "ChunkedWriteCallback.h",
     "CommandResponseHelper.h",
@@ -464,6 +477,7 @@
     ":event-reporter",
     ":global-attributes",
     ":interaction-model",
+    ":path-expansion",
     "${chip_root}/src/app/data-model",
     "${chip_root}/src/app/data-model-provider",
     "${chip_root}/src/app/icd/server:icd-server-config",
diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp
index 2fafe37..d4afe33 100644
--- a/src/app/InteractionModelEngine.cpp
+++ b/src/app/InteractionModelEngine.cpp
@@ -582,12 +582,14 @@
 
         if (paramsList.mValue.IsWildcardPath())
         {
-            AttributePathExpandIterator pathIterator(GetDataModelProvider(), &paramsList);
+
+            auto state = AttributePathExpandIterator::Position::StartIterating(&paramsList);
+            AttributePathExpandIterator pathIterator(GetDataModelProvider(), state);
             ConcreteAttributePath readPath;
 
             // The definition of "valid path" is "path exists and ACL allows access". The "path exists" part is handled by
             // AttributePathExpandIterator. So we just need to check the ACL bits.
-            for (; pathIterator.Get(readPath); pathIterator.Next())
+            while (pathIterator.Next(readPath))
             {
                 // leave requestPath.entityId optional value unset to indicate wildcard
                 Access::RequestPath requestPath{ .cluster     = readPath.mClusterId,
@@ -846,8 +848,7 @@
 
     // We have already reserved enough resources for read requests, and have granted enough resources for current subscriptions, so
     // we should be able to allocate resources requested by this request.
-    ReadHandler * handler =
-        mReadHandlers.CreateObject(*this, apExchangeContext, aInteractionType, mReportScheduler, GetDataModelProvider());
+    ReadHandler * handler = mReadHandlers.CreateObject(*this, apExchangeContext, aInteractionType, mReportScheduler);
     if (handler == nullptr)
     {
         ChipLogProgress(InteractionModel, "no resource for %s interaction",
diff --git a/src/app/ReadHandler.cpp b/src/app/ReadHandler.cpp
index 43f9b93..dbde46b 100644
--- a/src/app/ReadHandler.cpp
+++ b/src/app/ReadHandler.cpp
@@ -15,14 +15,8 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-
-/**
- *    @file
- *      This file defines read handler for a CHIP Interaction Data model
- *
- */
-
 #include <app/AppConfig.h>
+#include <app/AttributePathExpandIterator.h>
 #include <app/InteractionModelEngine.h>
 #include <app/MessageDef/EventPathIB.h>
 #include <app/MessageDef/StatusResponseMessage.h>
@@ -31,6 +25,7 @@
 #include <app/data-model-provider/Provider.h>
 #include <app/icd/server/ICDServerConfig.h>
 #include <lib/core/TLVUtilities.h>
+#include <lib/support/CodeUtils.h>
 #include <messaging/ExchangeContext.h>
 
 #include <app/ReadHandler.h>
@@ -54,9 +49,9 @@
 }
 
 ReadHandler::ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext,
-                         InteractionType aInteractionType, Observer * observer, DataModel::Provider * apDataModel) :
-    mAttributePathExpandIterator(apDataModel, nullptr),
-    mExchangeCtx(*this), mManagementCallback(apCallback)
+                         InteractionType aInteractionType, Observer * observer) :
+    mExchangeCtx(*this),
+    mManagementCallback(apCallback)
 {
     VerifyOrDie(apExchangeContext != nullptr);
 
@@ -80,8 +75,8 @@
 }
 
 #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
-ReadHandler::ReadHandler(ManagementCallback & apCallback, Observer * observer, DataModel::Provider * apDataModel) :
-    mAttributePathExpandIterator(apDataModel, nullptr), mExchangeCtx(*this), mManagementCallback(apCallback)
+ReadHandler::ReadHandler(ManagementCallback & apCallback, Observer * observer) :
+    mExchangeCtx(*this), mManagementCallback(apCallback)
 {
     mInteractionType = InteractionType::Subscribe;
     mFlags.ClearAll();
@@ -511,8 +506,8 @@
     if (CHIP_END_OF_TLV == err)
     {
         mManagementCallback.GetInteractionModelEngine()->RemoveDuplicateConcreteAttributePath(mpAttributePathList);
-        mAttributePathExpandIterator.ResetTo(mpAttributePathList);
-        err = CHIP_NO_ERROR;
+        mAttributePathExpandPosition = AttributePathExpandIterator::Position::StartIterating(mpAttributePathList);
+        err                          = CHIP_NO_ERROR;
     }
     return err;
 }
@@ -854,16 +849,18 @@
 
 void ReadHandler::ResetPathIterator()
 {
-    mAttributePathExpandIterator.ResetTo(mpAttributePathList);
+    mAttributePathExpandPosition = AttributePathExpandIterator::Position::StartIterating(mpAttributePathList);
     mAttributeEncoderState.Reset();
 }
 
-void ReadHandler::AttributePathIsDirty(const AttributePathParams & aAttributeChanged)
+void ReadHandler::AttributePathIsDirty(DataModel::Provider * apDataModel, const AttributePathParams & aAttributeChanged)
 {
-    ConcreteAttributePath path;
-
     mDirtyGeneration = mManagementCallback.GetInteractionModelEngine()->GetReportingEngine().GetDirtySetGeneration();
 
+    // We want to get the value, but not advance the iterator position.
+    AttributePathExpandIterator::Position tempPosition = mAttributePathExpandPosition;
+    ConcreteAttributePath path;
+
     // We won't reset the path iterator for every AttributePathIsDirty call to reduce the number of full data reports.
     // The iterator will be reset after finishing each report session.
     //
@@ -873,7 +870,7 @@
     // TODO (#16699): Currently we can only guarantee the reports generated from a single path in the request are consistent. The
     // data might be inconsistent if the user send a request with two paths from the same cluster. We need to clearify the behavior
     // or make it consistent.
-    if (mAttributePathExpandIterator.Get(path) &&
+    if (AttributePathExpandIterator(apDataModel, tempPosition).Next(path) &&
         (aAttributeChanged.HasWildcardEndpointId() || aAttributeChanged.mEndpointId == path.mEndpointId) &&
         (aAttributeChanged.HasWildcardClusterId() || aAttributeChanged.mClusterId == path.mClusterId))
     {
@@ -883,7 +880,8 @@
         // If we're currently in the middle of generating reports for a given cluster and that in turn is marked dirty, let's reset
         // our iterator to point back to the beginning of that cluster. This ensures that the receiver will get a coherent view of
         // the state of the cluster as present on the server
-        mAttributePathExpandIterator.ResetCurrentCluster();
+        mAttributePathExpandPosition.IterateFromTheStartOfTheCurrentClusterIfAttributeWildcard();
+
         mAttributeEncoderState.Reset();
     }
 
diff --git a/src/app/ReadHandler.h b/src/app/ReadHandler.h
index 85976ec..d5b7ad2 100644
--- a/src/app/ReadHandler.h
+++ b/src/app/ReadHandler.h
@@ -212,7 +212,7 @@
      *
      */
     ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, InteractionType aInteractionType,
-                Observer * observer, DataModel::Provider * apDataModel);
+                Observer * observer);
 
 #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
     /**
@@ -222,7 +222,7 @@
      *  The callback passed in has to outlive this handler object.
      *
      */
-    ReadHandler(ManagementCallback & apCallback, Observer * observer, DataModel::Provider * apDataModel);
+    ReadHandler(ManagementCallback & apCallback, Observer * observer);
 #endif
 
     const SingleLinkedListNode<AttributePathParams> * GetAttributePathList() const { return mpAttributePathList; }
@@ -407,12 +407,12 @@
     bool IsFabricFiltered() const { return mFlags.Has(ReadHandlerFlags::FabricFiltered); }
     CHIP_ERROR OnSubscribeRequest(Messaging::ExchangeContext * apExchangeContext, System::PacketBufferHandle && aPayload);
     void GetSubscriptionId(SubscriptionId & aSubscriptionId) const { aSubscriptionId = mSubscriptionId; }
-    AttributePathExpandIterator * GetAttributePathExpandIterator() { return &mAttributePathExpandIterator; }
+    AttributePathExpandIterator::Position & AttributeIterationPosition() { return mAttributePathExpandPosition; }
 
     /// @brief Notifies the read handler that a set of attribute paths has been marked dirty. This will schedule a reporting engine
     /// run if the change to the attribute path makes the ReadHandler reportable.
     /// @param aAttributeChanged Path to the attribute that was changed.
-    void AttributePathIsDirty(const AttributePathParams & aAttributeChanged);
+    void AttributePathIsDirty(DataModel::Provider * apDataModel, const AttributePathParams & aAttributeChanged);
     bool IsDirty() const
     {
         return (mDirtyGeneration > mPreviousReportsBeginGeneration) || mFlags.Has(ReadHandlerFlags::ForceDirty);
@@ -519,7 +519,7 @@
     /// @param aFlag Flag to clear
     void ClearStateFlag(ReadHandlerFlags aFlag);
 
-    AttributePathExpandIterator mAttributePathExpandIterator;
+    SubscriptionId mSubscriptionId = 0;
 
     // The current generation of the reporting engine dirty set the last time we were notified that a path we're interested in was
     // marked dirty.
@@ -561,18 +561,13 @@
     // engine, the "oldest" subscription is the subscription with the smallest generation.
     uint64_t mTransactionStartGeneration = 0;
 
-    SubscriptionId mSubscriptionId           = 0;
-    uint16_t mMinIntervalFloorSeconds        = 0;
-    uint16_t mMaxInterval                    = 0;
-    uint16_t mSubscriberRequestedMaxInterval = 0;
-
     EventNumber mEventMin = 0;
 
     // The last schedule event number snapshoted in the beginning when preparing to fill new events to reports
     EventNumber mLastScheduledEventNumber = 0;
 
-    // TODO: We should shutdown the transaction when the session expires.
-    SessionHolder mSessionHandle;
+    /// Iterator position state for any ongoing path expansion for handling wildcard reads/subscriptions.
+    AttributePathExpandIterator::Position mAttributePathExpandPosition;
 
     Messaging::ExchangeHolder mExchangeCtx;
 #if CHIP_CONFIG_UNSAFE_SUBSCRIPTION_EXCHANGE_MANAGER_USE
@@ -587,20 +582,26 @@
 
     ManagementCallback & mManagementCallback;
 
+    // TODO (#27675): Merge all observers into one and that one will dispatch the callbacks to the right place.
+    Observer * mObserver = nullptr;
+
     uint32_t mLastWrittenEventsBytes = 0;
 
     // The detailed encoding state for a single attribute, used by list chunking feature.
     // The size of AttributeEncoderState is 2 bytes for now.
     AttributeEncodeState mAttributeEncoderState;
 
+    uint16_t mMinIntervalFloorSeconds        = 0;
+    uint16_t mMaxInterval                    = 0;
+    uint16_t mSubscriberRequestedMaxInterval = 0;
+
     // Current Handler state
     HandlerState mState            = HandlerState::Idle;
     PriorityLevel mCurrentPriority = PriorityLevel::Invalid;
     BitFlags<ReadHandlerFlags> mFlags;
     InteractionType mInteractionType = InteractionType::Read;
 
-    // TODO (#27675): Merge all observers into one and that one will dispatch the callbacks to the right place.
-    Observer * mObserver = nullptr;
+    SessionHolder mSessionHandle;
 };
 
 } // namespace app
diff --git a/src/app/SubscriptionResumptionSessionEstablisher.cpp b/src/app/SubscriptionResumptionSessionEstablisher.cpp
index f9ea25c..6e01668 100644
--- a/src/app/SubscriptionResumptionSessionEstablisher.cpp
+++ b/src/app/SubscriptionResumptionSessionEstablisher.cpp
@@ -103,8 +103,7 @@
         ChipLogProgress(InteractionModel, "no resource for subscription resumption");
         return;
     }
-    ReadHandler * readHandler =
-        imEngine->mReadHandlers.CreateObject(*imEngine, imEngine->GetReportScheduler(), imEngine->GetDataModelProvider());
+    ReadHandler * readHandler = imEngine->mReadHandlers.CreateObject(*imEngine, imEngine->GetReportScheduler());
     if (readHandler == nullptr)
     {
         // TODO - Should we keep the subscription here?
diff --git a/src/app/reporting/Engine.cpp b/src/app/reporting/Engine.cpp
index a9cef5c..ed23d44 100644
--- a/src/app/reporting/Engine.cpp
+++ b/src/app/reporting/Engine.cpp
@@ -19,6 +19,7 @@
 #include <access/AccessRestrictionProvider.h>
 #include <access/Privilege.h>
 #include <app/AppConfig.h>
+#include <app/AttributePathExpandIterator.h>
 #include <app/ConcreteEventPath.h>
 #include <app/GlobalAttributes.h>
 #include <app/InteractionModelEngine.h>
@@ -315,8 +316,9 @@
 #endif
 
         // For each path included in the interested path of the read handler...
-        for (; apReadHandler->GetAttributePathExpandIterator()->Get(readPath);
-             apReadHandler->GetAttributePathExpandIterator()->Next())
+        for (RollbackAttributePathExpandIterator iterator(mpImEngine->GetDataModelProvider(),
+                                                          apReadHandler->AttributeIterationPosition());
+             iterator.Next(readPath); iterator.MarkCompleted())
         {
             if (!apReadHandler->IsPriming())
             {
@@ -428,6 +430,7 @@
             // Successfully encoded the attribute, clear the internal state.
             apReadHandler->SetAttributeEncodeState(AttributeEncodeState());
         }
+
         // We just visited all paths interested by this read handler and did not abort in the middle of iteration, there are no more
         // chunks for this report.
         hasMoreChunks = false;
@@ -1042,8 +1045,9 @@
 {
     BumpDirtySetGeneration();
 
-    bool intersectsInterestPath = false;
-    mpImEngine->mReadHandlers.ForEachActiveObject([&aAttributePath, &intersectsInterestPath](ReadHandler * handler) {
+    bool intersectsInterestPath     = false;
+    DataModel::Provider * dataModel = mpImEngine->GetDataModelProvider();
+    mpImEngine->mReadHandlers.ForEachActiveObject([&dataModel, &aAttributePath, &intersectsInterestPath](ReadHandler * handler) {
         // We call AttributePathIsDirty for both read interactions and subscribe interactions, since we may send inconsistent
         // attribute data between two chunks. AttributePathIsDirty will not schedule a new run for read handlers which are
         // waiting for a response to the last message chunk for read interactions.
@@ -1053,7 +1057,7 @@
             {
                 if (object->mValue.Intersects(aAttributePath))
                 {
-                    handler->AttributePathIsDirty(aAttributePath);
+                    handler->AttributePathIsDirty(dataModel, aAttributePath);
                     intersectsInterestPath = true;
                     break;
                 }
diff --git a/src/app/tests/TestAttributePathExpandIterator.cpp b/src/app/tests/TestAttributePathExpandIterator.cpp
index 913b07e..4a6088a 100644
--- a/src/app/tests/TestAttributePathExpandIterator.cpp
+++ b/src/app/tests/TestAttributePathExpandIterator.cpp
@@ -16,6 +16,7 @@
  *    limitations under the License.
  */
 
+#include "pw_unit_test/framework.h"
 #include <app-common/zap-generated/ids/Attributes.h>
 #include <app/AttributePathExpandIterator.h>
 #include <app/ConcreteAttributePath.h>
@@ -106,9 +107,18 @@
 
     size_t index = 0;
 
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo); iter.Get(path);
-         iter.Next())
+    auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+
+    while (true)
     {
+        // re-create the iterator
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        if (!iter.Next(path))
+        {
+            break;
+        }
+
         ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
                       ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
         EXPECT_LT(index, ArraySize(paths));
@@ -131,9 +141,16 @@
 
     size_t index = 0;
 
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo); iter.Get(path);
-         iter.Next())
+    auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+    while (true)
     {
+        // re-create the iterator
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        if (!iter.Next(path))
+        {
+            break;
+        }
         ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
                       ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
         EXPECT_LT(index, ArraySize(paths));
@@ -159,9 +176,16 @@
 
     size_t index = 0;
 
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo); iter.Get(path);
-         iter.Next())
+    auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+    while (true)
     {
+        // re-create the iterator
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        if (!iter.Next(path))
+        {
+            break;
+        }
         ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
                       ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
         EXPECT_LT(index, ArraySize(paths));
@@ -187,9 +211,17 @@
 
     size_t index = 0;
 
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo); iter.Get(path);
-         iter.Next())
+    auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+
+    while (true)
     {
+        // re-create the iterator
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        if (!iter.Next(path))
+        {
+            break;
+        }
         ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
                       ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
         EXPECT_LT(index, ArraySize(paths));
@@ -219,9 +251,17 @@
 
     size_t index = 0;
 
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo); iter.Get(path);
-         iter.Next())
+    auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+
+    while (true)
     {
+        // re-create the iterator
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        if (!iter.Next(path))
+        {
+            break;
+        }
         ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
                       ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
         EXPECT_LT(index, ArraySize(paths));
@@ -245,9 +285,16 @@
 
     size_t index = 0;
 
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo); iter.Get(path);
-         iter.Next())
+    auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+    while (true)
     {
+        // re-create the iterator
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        if (!iter.Next(path))
+        {
+            break;
+        }
         ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
                       ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
         EXPECT_LT(index, ArraySize(paths));
@@ -257,6 +304,69 @@
     EXPECT_EQ(index, ArraySize(paths));
 }
 
+TEST(TestAttributePathExpandIterator, TestFixedPathExpansion)
+{
+    // expansion logic requires that:
+    //   - paths for wildcard expansion ARE VALIDATED
+    //   - path WITHOUT wildcard expansion ARE NOT VALIDATED
+
+    // invalid attribute across all clusters returns empty
+    {
+        SingleLinkedListNode<app::AttributePathParams> clusInfo;
+        clusInfo.mValue.mAttributeId = 122333;
+
+        auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+        ConcreteAttributePath path;
+
+        EXPECT_FALSE(iter.Next(path));
+    }
+
+    // invalid cluster with a valid attribute (featuremap) returns empty
+    {
+        SingleLinkedListNode<app::AttributePathParams> clusInfo;
+        clusInfo.mValue.mClusterId   = 122344;
+        clusInfo.mValue.mAttributeId = Clusters::Globals::Attributes::FeatureMap::Id;
+
+        auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+        ConcreteAttributePath path;
+
+        EXPECT_FALSE(iter.Next(path));
+    }
+
+    // invalid cluster with wildcard attribute returns empty
+    {
+        SingleLinkedListNode<app::AttributePathParams> clusInfo;
+        clusInfo.mValue.mClusterId = 122333;
+
+        auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+        ConcreteAttributePath path;
+
+        EXPECT_FALSE(iter.Next(path));
+    }
+
+    // even though all above WERE invalid, if we specify a non-wildcard path it is returned as-is
+    {
+        SingleLinkedListNode<app::AttributePathParams> clusInfo;
+        clusInfo.mValue.mEndpointId  = 1;
+        clusInfo.mValue.mClusterId   = 122344;
+        clusInfo.mValue.mAttributeId = 122333;
+
+        auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo);
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+        ConcreteAttributePath path;
+
+        EXPECT_TRUE(iter.Next(path));
+        EXPECT_EQ(path.mEndpointId, clusInfo.mValue.mEndpointId);
+        EXPECT_EQ(path.mClusterId, clusInfo.mValue.mClusterId);
+        EXPECT_EQ(path.mAttributeId, clusInfo.mValue.mAttributeId);
+
+        EXPECT_FALSE(iter.Next(path));
+    }
+}
+
 TEST(TestAttributePathExpandIterator, TestMultipleClusInfo)
 {
 
@@ -358,18 +468,47 @@
         { kMockEndpoint2, MockClusterId(3), MockAttributeId(3) },
     };
 
-    size_t index = 0;
-
-    for (app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), &clusInfo1);
-         iter.Get(path); iter.Next())
+    // Test that a one-shot iterate through all works
     {
-        ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
-                      ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
-        EXPECT_LT(index, ArraySize(paths));
-        EXPECT_EQ(paths[index], path);
-        index++;
+        size_t index = 0;
+
+        auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo1);
+        app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+        while (iter.Next(path))
+        {
+            ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
+                          ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
+            EXPECT_LT(index, ArraySize(paths));
+            EXPECT_EQ(paths[index], path);
+            index++;
+        }
+        EXPECT_EQ(index, ArraySize(paths));
     }
-    EXPECT_EQ(index, ArraySize(paths));
+
+    // identical test, however this checks that position re-use works
+    // the same as a one-shot iteration.
+    {
+        size_t index = 0;
+
+        auto position = AttributePathExpandIterator::Position::StartIterating(&clusInfo1);
+        while (true)
+        {
+            // re-create the iterator
+            app::AttributePathExpandIterator iter(CodegenDataModelProviderInstance(nullptr /* delegate */), position);
+
+            if (!iter.Next(path))
+            {
+                break;
+            }
+            ChipLogDetail(AppServer, "Visited Attribute: 0x%04X / " ChipLogFormatMEI " / " ChipLogFormatMEI, path.mEndpointId,
+                          ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId));
+            EXPECT_LT(index, ArraySize(paths));
+            EXPECT_EQ(paths[index], path);
+            index++;
+        }
+        EXPECT_EQ(index, ArraySize(paths));
+    }
 }
 
 } // namespace
diff --git a/src/app/tests/TestInteractionModelEngine.cpp b/src/app/tests/TestInteractionModelEngine.cpp
index 87aa3e3..d09f564 100644
--- a/src/app/tests/TestInteractionModelEngine.cpp
+++ b/src/app/tests/TestInteractionModelEngine.cpp
@@ -271,8 +271,7 @@
 
     // Create and setup readHandler 1
     ReadHandler * readHandler1 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that Bob still doesn't have an active subscription
     EXPECT_FALSE(engine->SubjectHasActiveSubscription(bobFabricIndex, bobNodeId));
@@ -319,16 +318,14 @@
 
     // Create readHandler 1
     engine->GetReadHandlerPool().CreateObject(nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe,
-                                              reporting::GetDefaultReportScheduler(),
-                                              CodegenDataModelProviderInstance(nullptr /* delegate */));
+                                              reporting::GetDefaultReportScheduler());
 
     // Verify that Bob still doesn't have an active subscription
     EXPECT_FALSE(engine->SubjectHasActiveSubscription(bobFabricIndex, bobNodeId));
 
     // Create and setup readHandler 2
     ReadHandler * readHandler2 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that Bob still doesn't have an active subscription
     EXPECT_FALSE(engine->SubjectHasActiveSubscription(bobFabricIndex, bobNodeId));
@@ -380,13 +377,11 @@
 
     // Create and setup readHandler 1
     ReadHandler * readHandler1 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Create and setup readHandler 2
     ReadHandler * readHandler2 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that Bob still doesn't have an active subscription
     EXPECT_FALSE(engine->SubjectHasActiveSubscription(bobFabricIndex, bobNodeId));
@@ -463,23 +458,19 @@
 
     // Create and setup readHandler 1-1
     engine->GetReadHandlerPool().CreateObject(nullCallback, exchangeCtx11, ReadHandler::InteractionType::Subscribe,
-                                              reporting::GetDefaultReportScheduler(),
-                                              CodegenDataModelProviderInstance(nullptr /* delegate */));
+                                              reporting::GetDefaultReportScheduler());
 
     // Create and setup readHandler 1-2
     ReadHandler * readHandler12 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx12, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx12, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Create and setup readHandler 2-1
     engine->GetReadHandlerPool().CreateObject(nullCallback, exchangeCtx21, ReadHandler::InteractionType::Subscribe,
-                                              reporting::GetDefaultReportScheduler(),
-                                              CodegenDataModelProviderInstance(nullptr /* delegate */));
+                                              reporting::GetDefaultReportScheduler());
 
     // Create and setup readHandler 2-2
     ReadHandler * readHandler22 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx22, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx22, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that both Alice and Bob have no active subscriptions
     EXPECT_FALSE(engine->SubjectHasActiveSubscription(bobFabricIndex, bobNodeId));
@@ -551,8 +542,7 @@
 
     // Create readHandler
     ReadHandler * readHandler = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify there are not active subscriptions
     EXPECT_FALSE(engine->SubjectHasActiveSubscription(bobFabricIndex, valideSubjectId));
@@ -749,8 +739,7 @@
 
     // Create and setup readHandler 1
     ReadHandler * readHandler1 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that fabric 1 still doesn't have an active subscription
     EXPECT_FALSE(engine->FabricHasAtLeastOneActiveSubscription(fabricIndex1));
@@ -766,8 +755,7 @@
 
     // Create and setup readHandler 2
     ReadHandler * readHandler2 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Set readHandler2 to active
     readHandler2->SetStateFlag(ReadHandler::ReadHandlerFlags::ActiveSubscription, true);
@@ -805,8 +793,7 @@
 
     // Create and setup readHandler 1
     ReadHandler * readHandler1 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx1, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that the fabric still doesn't have an active subscription
     EXPECT_FALSE(engine->FabricHasAtLeastOneActiveSubscription(fabricIndex));
@@ -819,8 +806,7 @@
 
     // Create and setup readHandler 2
     ReadHandler * readHandler2 = engine->GetReadHandlerPool().CreateObject(
-        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler(),
-        CodegenDataModelProviderInstance(nullptr /* delegate */));
+        nullCallback, exchangeCtx2, ReadHandler::InteractionType::Subscribe, reporting::GetDefaultReportScheduler());
 
     // Verify that the fabric still has an active subscription
     EXPECT_TRUE(engine->FabricHasAtLeastOneActiveSubscription(fabricIndex));
diff --git a/src/app/tests/TestReadInteraction.cpp b/src/app/tests/TestReadInteraction.cpp
index 21b15d1..b56db3e 100644
--- a/src/app/tests/TestReadInteraction.cpp
+++ b/src/app/tests/TestReadInteraction.cpp
@@ -565,8 +565,7 @@
 
     {
         Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
-        ReadHandler readHandler(nullCallback, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(nullCallback, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         GenerateReportData(reportDatabuf, ReportType::kValid, false /* aSuppressResponse*/);
         EXPECT_EQ(readHandler.SendReportData(std::move(reportDatabuf), false), CHIP_ERROR_INCORRECT_STATE);
@@ -624,8 +623,7 @@
         uint16_t maxInterval;
 
         // Configure ReadHandler
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
@@ -842,8 +840,7 @@
 
     {
         Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
-        ReadHandler readHandler(nullCallback, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(nullCallback, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         GenerateReportData(reportDatabuf, ReportType::kValid, false /* aSuppressResponse*/);
         EXPECT_EQ(readHandler.SendReportData(std::move(reportDatabuf), false), CHIP_ERROR_INCORRECT_STATE);
@@ -1592,8 +1589,7 @@
     Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
 
     {
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
@@ -1654,8 +1650,7 @@
     Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
 
     {
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
@@ -1724,8 +1719,7 @@
     Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
 
     {
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
@@ -1794,8 +1788,7 @@
     Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
 
     {
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
@@ -1864,8 +1857,7 @@
     Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
 
     {
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
@@ -1932,8 +1924,7 @@
     Messaging::ExchangeContext * exchangeCtx = NewExchangeToAlice(nullptr, false);
 
     {
-        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler,
-                                CodegenDataModelProviderInstance(nullptr /* delegate */));
+        ReadHandler readHandler(*engine, exchangeCtx, chip::app::ReadHandler::InteractionType::Read, gReportScheduler);
 
         writer.Init(std::move(subscribeRequestbuf));
         EXPECT_EQ(subscribeRequestBuilder.Init(&writer), CHIP_NO_ERROR);
diff --git a/src/app/tests/TestReportScheduler.cpp b/src/app/tests/TestReportScheduler.cpp
index 03dda96..7047dd6 100644
--- a/src/app/tests/TestReportScheduler.cpp
+++ b/src/app/tests/TestReportScheduler.cpp
@@ -276,8 +276,7 @@
     for (size_t i = 0; i < kNumMaxReadHandlers; i++)
     {
         ReadHandler * readHandler =
-            readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler,
-                                         CodegenDataModelProviderInstance(nullptr /* delegate */));
+            readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler);
         sScheduler.OnSubscriptionEstablished(readHandler);
         ASSERT_NE(nullptr, readHandler);
         ASSERT_NE(nullptr, sScheduler.FindReadHandlerNode(readHandler));
@@ -341,22 +340,19 @@
     // Dirty read handler, will be triggered at min interval
     // Test OnReadHandler created
     ReadHandler * readHandler1 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler);
 
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler1, &sScheduler, 1, 2));
     readHandler1->ForceDirtyState();
 
     // Clean read handler, will be triggered at max interval
     ReadHandler * readHandler2 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler);
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler2, &sScheduler, 0, 3));
 
     // Clean read handler, will be triggered at max interval, but will be cancelled before
     ReadHandler * readHandler3 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler);
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler3, &sScheduler, 0, 3));
 
     // Confirms that none of the ReadHandlers are currently reportable
@@ -409,8 +405,8 @@
     // Initialize mock timestamp
     sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
 
-    ReadHandler * readHandler = readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe,
-                                                             &sScheduler, CodegenDataModelProviderInstance(nullptr /* delegate */));
+    ReadHandler * readHandler =
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &sScheduler);
 
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler, &sScheduler, 1, 2));
 
@@ -486,15 +482,13 @@
     sTestTimerSynchronizedDelegate.SetMockSystemTimestamp(System::Clock::Milliseconds64(0));
 
     ReadHandler * readHandler1 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler);
 
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler1, &syncScheduler, 0, 2));
     ReadHandlerNode * node1 = syncScheduler.FindReadHandlerNode(readHandler1);
 
     ReadHandler * readHandler2 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler);
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler2, &syncScheduler, 1, 3));
 
     ReadHandlerNode * node2 = syncScheduler.FindReadHandlerNode(readHandler2);
@@ -622,8 +616,7 @@
     sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000));
 
     ReadHandler * readHandler3 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler);
 
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler3, &syncScheduler, 2, 3));
     ReadHandlerNode * node3 = syncScheduler.FindReadHandlerNode(readHandler3);
@@ -677,8 +670,7 @@
 
     // Now simulate a new readHandler being added with a max forcing a conflict
     ReadHandler * readHandler4 =
-        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler,
-                                     CodegenDataModelProviderInstance(nullptr /* delegate */));
+        readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe, &syncScheduler);
 
     EXPECT_EQ(CHIP_NO_ERROR, MockReadHandlerSubscriptionTransaction(readHandler4, &syncScheduler, 0, 1));
     ReadHandlerNode * node4 = syncScheduler.FindReadHandlerNode(readHandler4);
diff --git a/src/app/tests/TestReportingEngine.cpp b/src/app/tests/TestReportingEngine.cpp
index 60852e2..bfeb961 100644
--- a/src/app/tests/TestReportingEngine.cpp
+++ b/src/app/tests/TestReportingEngine.cpp
@@ -209,8 +209,7 @@
     EXPECT_EQ(readRequestBuilder.GetError(), CHIP_NO_ERROR);
     EXPECT_EQ(writer.Finalize(&readRequestbuf), CHIP_NO_ERROR);
     app::ReadHandler readHandler(dummy, exchangeCtx, chip::app::ReadHandler::InteractionType::Read,
-                                 app::reporting::GetDefaultReportScheduler(),
-                                 CodegenDataModelProviderInstance(nullptr /* delegate */));
+                                 app::reporting::GetDefaultReportScheduler());
     readHandler.OnInitialRequest(std::move(readRequestbuf));
 
     EXPECT_EQ(InteractionModelEngine::GetInstance()->GetReportingEngine().BuildAndSendSingleReportData(&readHandler),