mgmt: redesign ControlCommand and its usage in nfd::Controller

Split parameter encoding and validation into a separate class so that
different commands can have different formats in the future. Moreover,
request parameters and response parameters may be of different types.

Lastly, change the hierarchy to CRTP and make everything static. All the
encoding and validation details are the same for every request/response
of a given command type, so it makes no sense to allocate a separate
ControlCommand instance for each individual request.

Change-Id: I56c16dc3e275aaa48608478aad002d448c0492cc
diff --git a/docs/conf.py b/docs/conf.py
index 8ea637a..09af1d0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,7 +10,7 @@
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 
 project = 'ndn-cxx: NDN C++ library with eXperimental eXtensions'
-copyright = 'Copyright © 2013-2024 Regents of the University of California.'
+copyright = 'Copyright © 2013-2025 Regents of the University of California.'
 author = 'Named Data Networking Project'
 
 # The short X.Y version.
diff --git a/ndn-cxx/impl/face-impl.hpp b/ndn-cxx/impl/face-impl.hpp
index 2b08d5e..a07a9f4 100644
--- a/ndn-cxx/impl/face-impl.hpp
+++ b/ndn-cxx/impl/face-impl.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -30,7 +30,7 @@
 #include "ndn-cxx/lp/fields.hpp"
 #include "ndn-cxx/lp/packet.hpp"
 #include "ndn-cxx/lp/tags.hpp"
-#include "ndn-cxx/mgmt/nfd/command-options.hpp"
+#include "ndn-cxx/mgmt/nfd/control-command.hpp"
 #include "ndn-cxx/mgmt/nfd/controller.hpp"
 #include "ndn-cxx/transport/transport.hpp"
 #include "ndn-cxx/util/logger.hpp"
diff --git a/ndn-cxx/mgmt/nfd/control-command.cpp b/ndn-cxx/mgmt/nfd/control-command.cpp
index 352fa4c..5e3e09e 100644
--- a/ndn-cxx/mgmt/nfd/control-command.cpp
+++ b/ndn-cxx/mgmt/nfd/control-command.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -23,56 +23,14 @@
 
 namespace ndn::nfd {
 
-ControlCommand::ControlCommand(const std::string& module, const std::string& verb)
-  : m_module(module)
-  , m_verb(verb)
-{
-}
-
-ControlCommand::~ControlCommand() = default;
-
-void
-ControlCommand::validateRequest(const ControlParameters& parameters) const
-{
-  m_requestValidator.validate(parameters);
-}
-
-void
-ControlCommand::applyDefaultsToRequest(ControlParameters&) const
-{
-}
-
-void
-ControlCommand::validateResponse(const ControlParameters& parameters) const
-{
-  m_responseValidator.validate(parameters);
-}
-
-void
-ControlCommand::applyDefaultsToResponse(ControlParameters&) const
-{
-}
-
-Name
-ControlCommand::getRequestName(const Name& commandPrefix,
-                               const ControlParameters& parameters) const
-{
-  this->validateRequest(parameters);
-
-  return Name(commandPrefix)
-         .append(m_module)
-         .append(m_verb)
-         .append(parameters.wireEncode());
-}
-
-ControlCommand::FieldValidator::FieldValidator()
+ControlParametersCommandFormat::ControlParametersCommandFormat()
   : m_required(CONTROL_PARAMETER_UBOUND)
   , m_optional(CONTROL_PARAMETER_UBOUND)
 {
 }
 
 void
-ControlCommand::FieldValidator::validate(const ControlParameters& parameters) const
+ControlParametersCommandFormat::validate(const ControlParameters& parameters) const
 {
   const auto& presentFields = parameters.getPresentFields();
 
@@ -95,31 +53,37 @@
   }
 }
 
-FaceCreateCommand::FaceCreateCommand()
-  : ControlCommand("faces", "create")
+void
+ControlParametersCommandFormat::encode(Interest& interest, const ControlParameters& params) const
 {
-  m_requestValidator
+  auto name = interest.getName();
+  name.append(params.wireEncode());
+  interest.setName(name);
+}
+
+const FaceCreateCommand::RequestFormat FaceCreateCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_URI)
     .optional(CONTROL_PARAMETER_LOCAL_URI)
+    .optional(CONTROL_PARAMETER_FLAGS)
+    .optional(CONTROL_PARAMETER_MASK)
     .optional(CONTROL_PARAMETER_FACE_PERSISTENCY)
     .optional(CONTROL_PARAMETER_BASE_CONGESTION_MARKING_INTERVAL)
     .optional(CONTROL_PARAMETER_DEFAULT_CONGESTION_THRESHOLD)
-    .optional(CONTROL_PARAMETER_MTU)
-    .optional(CONTROL_PARAMETER_FLAGS)
-    .optional(CONTROL_PARAMETER_MASK);
-  m_responseValidator
+    .optional(CONTROL_PARAMETER_MTU);
+const FaceCreateCommand::ResponseFormat FaceCreateCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_FACE_ID)
     .required(CONTROL_PARAMETER_URI)
     .required(CONTROL_PARAMETER_LOCAL_URI)
+    .required(CONTROL_PARAMETER_FLAGS)
     .required(CONTROL_PARAMETER_FACE_PERSISTENCY)
     .optional(CONTROL_PARAMETER_BASE_CONGESTION_MARKING_INTERVAL)
     .optional(CONTROL_PARAMETER_DEFAULT_CONGESTION_THRESHOLD)
-    .optional(CONTROL_PARAMETER_MTU)
-    .required(CONTROL_PARAMETER_FLAGS);
-}
+    .optional(CONTROL_PARAMETER_MTU);
 
 void
-FaceCreateCommand::applyDefaultsToRequest(ControlParameters& parameters) const
+FaceCreateCommand::applyDefaultsToRequestImpl(ControlParameters& parameters)
 {
   if (!parameters.hasFacePersistency()) {
     parameters.setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
@@ -127,37 +91,33 @@
 }
 
 void
-FaceCreateCommand::validateResponse(const ControlParameters& parameters) const
+FaceCreateCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
 }
 
-FaceUpdateCommand::FaceUpdateCommand()
-  : ControlCommand("faces", "update")
-{
-  m_requestValidator
+const FaceUpdateCommand::RequestFormat FaceUpdateCommand::s_requestFormat =
+    RequestFormat()
     .optional(CONTROL_PARAMETER_FACE_ID)
+    .optional(CONTROL_PARAMETER_FLAGS)
+    .optional(CONTROL_PARAMETER_MASK)
     .optional(CONTROL_PARAMETER_FACE_PERSISTENCY)
     .optional(CONTROL_PARAMETER_BASE_CONGESTION_MARKING_INTERVAL)
     .optional(CONTROL_PARAMETER_DEFAULT_CONGESTION_THRESHOLD)
-    .optional(CONTROL_PARAMETER_MTU)
-    .optional(CONTROL_PARAMETER_FLAGS)
-    .optional(CONTROL_PARAMETER_MASK);
-  m_responseValidator
+    .optional(CONTROL_PARAMETER_MTU);
+const FaceUpdateCommand::ResponseFormat FaceUpdateCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_FACE_ID)
+    .required(CONTROL_PARAMETER_FLAGS)
     .required(CONTROL_PARAMETER_FACE_PERSISTENCY)
     .optional(CONTROL_PARAMETER_BASE_CONGESTION_MARKING_INTERVAL)
     .optional(CONTROL_PARAMETER_DEFAULT_CONGESTION_THRESHOLD)
-    .optional(CONTROL_PARAMETER_MTU)
-    .required(CONTROL_PARAMETER_FLAGS);
-}
+    .optional(CONTROL_PARAMETER_MTU);
 
 void
-FaceUpdateCommand::applyDefaultsToRequest(ControlParameters& parameters) const
+FaceUpdateCommand::applyDefaultsToRequestImpl(ControlParameters& parameters)
 {
   if (!parameters.hasFaceId()) {
     parameters.setFaceId(0);
@@ -165,54 +125,47 @@
 }
 
 void
-FaceUpdateCommand::validateResponse(const ControlParameters& parameters) const
+FaceUpdateCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
 }
 
-FaceDestroyCommand::FaceDestroyCommand()
-  : ControlCommand("faces", "destroy")
-{
-  m_requestValidator
+const FaceDestroyCommand::RequestFormat FaceDestroyCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_FACE_ID);
-  m_responseValidator = m_requestValidator;
-}
+const FaceDestroyCommand::ResponseFormat FaceDestroyCommand::s_responseFormat =
+    ResponseFormat()
+    .required(CONTROL_PARAMETER_FACE_ID);
 
 void
-FaceDestroyCommand::validateRequest(const ControlParameters& parameters) const
+FaceDestroyCommand::validateRequestImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateRequest(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
 }
 
 void
-FaceDestroyCommand::validateResponse(const ControlParameters& parameters) const
+FaceDestroyCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->validateRequest(parameters);
+  validateRequestImpl(parameters);
 }
 
-FibAddNextHopCommand::FibAddNextHopCommand()
-  : ControlCommand("fib", "add-nexthop")
-{
-  m_requestValidator
+const FibAddNextHopCommand::RequestFormat FibAddNextHopCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME)
     .optional(CONTROL_PARAMETER_FACE_ID)
     .optional(CONTROL_PARAMETER_COST);
-  m_responseValidator
+const FibAddNextHopCommand::ResponseFormat FibAddNextHopCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_NAME)
     .required(CONTROL_PARAMETER_FACE_ID)
     .required(CONTROL_PARAMETER_COST);
-}
 
 void
-FibAddNextHopCommand::applyDefaultsToRequest(ControlParameters& parameters) const
+FibAddNextHopCommand::applyDefaultsToRequestImpl(ControlParameters& parameters)
 {
   if (!parameters.hasFaceId()) {
     parameters.setFaceId(0);
@@ -223,28 +176,24 @@
 }
 
 void
-FibAddNextHopCommand::validateResponse(const ControlParameters& parameters) const
+FibAddNextHopCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
 }
 
-FibRemoveNextHopCommand::FibRemoveNextHopCommand()
-  : ControlCommand("fib", "remove-nexthop")
-{
-  m_requestValidator
+const FibRemoveNextHopCommand::RequestFormat FibRemoveNextHopCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME)
     .optional(CONTROL_PARAMETER_FACE_ID);
-  m_responseValidator
+const FibRemoveNextHopCommand::ResponseFormat FibRemoveNextHopCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_NAME)
     .required(CONTROL_PARAMETER_FACE_ID);
-}
 
 void
-FibRemoveNextHopCommand::applyDefaultsToRequest(ControlParameters& parameters) const
+FibRemoveNextHopCommand::applyDefaultsToRequestImpl(ControlParameters& parameters)
 {
   if (!parameters.hasFaceId()) {
     parameters.setFaceId(0);
@@ -252,113 +201,98 @@
 }
 
 void
-FibRemoveNextHopCommand::validateResponse(const ControlParameters& parameters) const
+FibRemoveNextHopCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
 }
 
-CsConfigCommand::CsConfigCommand()
-  : ControlCommand("cs", "config")
-{
-  m_requestValidator
+const CsConfigCommand::RequestFormat CsConfigCommand::s_requestFormat =
+    RequestFormat()
     .optional(CONTROL_PARAMETER_CAPACITY)
     .optional(CONTROL_PARAMETER_FLAGS)
     .optional(CONTROL_PARAMETER_MASK);
-  m_responseValidator
+const CsConfigCommand::ResponseFormat CsConfigCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_CAPACITY)
     .required(CONTROL_PARAMETER_FLAGS);
-}
 
-CsEraseCommand::CsEraseCommand()
-  : ControlCommand("cs", "erase")
-{
-  m_requestValidator
+const CsEraseCommand::RequestFormat CsEraseCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME)
     .optional(CONTROL_PARAMETER_COUNT);
-  m_responseValidator
+const CsEraseCommand::ResponseFormat CsEraseCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_NAME)
     .optional(CONTROL_PARAMETER_CAPACITY)
     .required(CONTROL_PARAMETER_COUNT);
-}
 
 void
-CsEraseCommand::validateRequest(const ControlParameters& parameters) const
+CsEraseCommand::validateRequestImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateRequest(parameters);
-
   if (parameters.hasCount() && parameters.getCount() == 0) {
     NDN_THROW(ArgumentError("Count must be positive"));
   }
 }
 
 void
-CsEraseCommand::validateResponse(const ControlParameters& parameters) const
+CsEraseCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.hasCapacity() && parameters.getCapacity() == 0) {
     NDN_THROW(ArgumentError("Capacity must be positive"));
   }
 }
 
-StrategyChoiceSetCommand::StrategyChoiceSetCommand()
-  : ControlCommand("strategy-choice", "set")
-{
-  m_requestValidator
+const StrategyChoiceSetCommand::RequestFormat StrategyChoiceSetCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME)
     .required(CONTROL_PARAMETER_STRATEGY);
-  m_responseValidator = m_requestValidator;
-}
+const StrategyChoiceSetCommand::ResponseFormat StrategyChoiceSetCommand::s_responseFormat =
+    ResponseFormat()
+    .required(CONTROL_PARAMETER_NAME)
+    .required(CONTROL_PARAMETER_STRATEGY);
 
-StrategyChoiceUnsetCommand::StrategyChoiceUnsetCommand()
-  : ControlCommand("strategy-choice", "unset")
-{
-  m_requestValidator
+const StrategyChoiceUnsetCommand::RequestFormat StrategyChoiceUnsetCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME);
-  m_responseValidator = m_requestValidator;
-}
+const StrategyChoiceUnsetCommand::ResponseFormat StrategyChoiceUnsetCommand::s_responseFormat =
+    ResponseFormat()
+    .required(CONTROL_PARAMETER_NAME);
 
 void
-StrategyChoiceUnsetCommand::validateRequest(const ControlParameters& parameters) const
+StrategyChoiceUnsetCommand::validateRequestImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateRequest(parameters);
-
   if (parameters.getName().empty()) {
     NDN_THROW(ArgumentError("Name must not be ndn:/"));
   }
 }
 
 void
-StrategyChoiceUnsetCommand::validateResponse(const ControlParameters& parameters) const
+StrategyChoiceUnsetCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->validateRequest(parameters);
+  validateRequestImpl(parameters);
 }
 
-RibRegisterCommand::RibRegisterCommand()
-  : ControlCommand("rib", "register")
-{
-  m_requestValidator
+const RibRegisterCommand::RequestFormat RibRegisterCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME)
     .optional(CONTROL_PARAMETER_FACE_ID)
     .optional(CONTROL_PARAMETER_ORIGIN)
     .optional(CONTROL_PARAMETER_COST)
     .optional(CONTROL_PARAMETER_FLAGS)
     .optional(CONTROL_PARAMETER_EXPIRATION_PERIOD);
-  m_responseValidator
+const RibRegisterCommand::ResponseFormat RibRegisterCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_NAME)
     .required(CONTROL_PARAMETER_FACE_ID)
     .required(CONTROL_PARAMETER_ORIGIN)
     .required(CONTROL_PARAMETER_COST)
     .required(CONTROL_PARAMETER_FLAGS)
     .optional(CONTROL_PARAMETER_EXPIRATION_PERIOD);
-}
 
 void
-RibRegisterCommand::applyDefaultsToRequest(ControlParameters& parameters) const
+RibRegisterCommand::applyDefaultsToRequestImpl(ControlParameters& parameters)
 {
   if (!parameters.hasFaceId()) {
     parameters.setFaceId(0);
@@ -375,30 +309,26 @@
 }
 
 void
-RibRegisterCommand::validateResponse(const ControlParameters& parameters) const
+RibRegisterCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
 }
 
-RibUnregisterCommand::RibUnregisterCommand()
-  : ControlCommand("rib", "unregister")
-{
-  m_requestValidator
+const RibUnregisterCommand::RequestFormat RibUnregisterCommand::s_requestFormat =
+    RequestFormat()
     .required(CONTROL_PARAMETER_NAME)
     .optional(CONTROL_PARAMETER_FACE_ID)
     .optional(CONTROL_PARAMETER_ORIGIN);
-  m_responseValidator
+const RibUnregisterCommand::ResponseFormat RibUnregisterCommand::s_responseFormat =
+    ResponseFormat()
     .required(CONTROL_PARAMETER_NAME)
     .required(CONTROL_PARAMETER_FACE_ID)
     .required(CONTROL_PARAMETER_ORIGIN);
-}
 
 void
-RibUnregisterCommand::applyDefaultsToRequest(ControlParameters& parameters) const
+RibUnregisterCommand::applyDefaultsToRequestImpl(ControlParameters& parameters)
 {
   if (!parameters.hasFaceId()) {
     parameters.setFaceId(0);
@@ -409,10 +339,8 @@
 }
 
 void
-RibUnregisterCommand::validateResponse(const ControlParameters& parameters) const
+RibUnregisterCommand::validateResponseImpl(const ControlParameters& parameters)
 {
-  this->ControlCommand::validateResponse(parameters);
-
   if (parameters.getFaceId() == INVALID_FACE_ID) {
     NDN_THROW(ArgumentError("FaceId must be valid"));
   }
diff --git a/ndn-cxx/mgmt/nfd/control-command.hpp b/ndn-cxx/mgmt/nfd/control-command.hpp
index a01c9a9..b7d1384 100644
--- a/ndn-cxx/mgmt/nfd/control-command.hpp
+++ b/ndn-cxx/mgmt/nfd/control-command.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -22,128 +22,198 @@
 #ifndef NDN_CXX_MGMT_NFD_CONTROL_COMMAND_HPP
 #define NDN_CXX_MGMT_NFD_CONTROL_COMMAND_HPP
 
+#include "ndn-cxx/interest.hpp"
 #include "ndn-cxx/mgmt/nfd/control-parameters.hpp"
 
 namespace ndn::nfd {
 
 /**
  * \ingroup management
- * \brief Base class of NFD `%ControlCommand`.
- * \sa https://redmine.named-data.net/projects/nfd/wiki/ControlCommand
+ * \brief Represents an error in the parameters of the control command request or response.
  */
-class ControlCommand : noncopyable
+class ArgumentError : public std::invalid_argument
 {
 public:
-  /** \brief Represents an error in ControlParameters.
+  using std::invalid_argument::invalid_argument;
+};
+
+
+/**
+ * \ingroup management
+ * \brief Implements encoding and validation of the ControlParameters format for control commands.
+ */
+class ControlParametersCommandFormat
+{
+public:
+  using ParametersType = ControlParameters;
+
+  ControlParametersCommandFormat();
+
+  /**
+   * \brief Declare a required field.
    */
-  class ArgumentError : public std::invalid_argument
+  ControlParametersCommandFormat&
+  required(ControlParameterField field)
   {
-  public:
-    using std::invalid_argument::invalid_argument;
-  };
+    m_required[field] = true;
+    return *this;
+  }
 
-  virtual
-  ~ControlCommand();
-
-  /** \brief Validate request parameters.
-   *  \throw ArgumentError if parameters are invalid
+  /**
+   * \brief Declare an optional field.
    */
-  virtual void
-  validateRequest(const ControlParameters& parameters) const;
-
-  /** \brief Apply default values to missing fields in request.
-   */
-  virtual void
-  applyDefaultsToRequest(ControlParameters& parameters) const;
-
-  /** \brief Validate response parameters.
-   *  \throw ArgumentError if parameters are invalid
-   */
-  virtual void
-  validateResponse(const ControlParameters& parameters) const;
-
-  /** \brief Apply default values to missing fields in response.
-   */
-  virtual void
-  applyDefaultsToResponse(ControlParameters& parameters) const;
-
-  /** \brief Construct the Name for a request Interest.
-   *  \throw ArgumentError if parameters are invalid
-   */
-  Name
-  getRequestName(const Name& commandPrefix, const ControlParameters& parameters) const;
-
-protected:
-  ControlCommand(const std::string& module, const std::string& verb);
-
-  class FieldValidator
+  ControlParametersCommandFormat&
+  optional(ControlParameterField field)
   {
-  public:
-    FieldValidator();
+    m_optional[field] = true;
+    return *this;
+  }
 
-    /** \brief Declare a required field.
-     */
-    FieldValidator&
-    required(ControlParameterField field)
-    {
-      m_required[field] = true;
-      return *this;
-    }
-
-    /** \brief Declare an optional field.
-     */
-    FieldValidator&
-    optional(ControlParameterField field)
-    {
-      m_optional[field] = true;
-      return *this;
-    }
-
-    /** \brief Verify that all required fields are present,
-     *         and all present fields are either required or optional.
-     *  \throw ArgumentError
-     */
-    void
-    validate(const ControlParameters& parameters) const;
-
-  private:
-    std::vector<bool> m_required;
-    std::vector<bool> m_optional;
-  };
-
-protected:
-  /** \brief FieldValidator for request ControlParameters.
-   *
-   *  Constructor of subclass should populate this validator.
+  /**
+   * \brief Verify that all required fields are present, and all present fields
+   *        are either required or optional.
+   * \throw ArgumentError Parameters validation failed.
    */
-  FieldValidator m_requestValidator;
-  /** \brief FieldValidator for response ControlParameters.
-   *
-   *  Constructor of subclass should populate this validator.
+  void
+  validate(const ControlParameters& params) const;
+
+  /**
+   * \brief Serializes the parameters into the request \p interest.
+   * \pre \p params are valid.
    */
-  FieldValidator m_responseValidator;
+  void
+  encode(Interest& interest, const ControlParameters& params) const;
 
 private:
-  name::Component m_module;
-  name::Component m_verb;
+  std::vector<bool> m_required;
+  std::vector<bool> m_optional;
 };
 
 
 /**
  * \ingroup management
+ * \brief Base class for all NFD control commands.
+ * \tparam RequestFormatType  A class type that will handle the encoding and validation of the request
+ *                            parameters. Only ControlParametersCommandFormat is supported for now.
+ * \tparam ResponseFormatType A class type that will handle the encoding and validation of the response
+ *                            parameters. Only ControlParametersCommandFormat is supported for now.
+ * \sa https://redmine.named-data.net/projects/nfd/wiki/ControlCommand
+ */
+template<typename Derived,
+         typename RequestFormatType = ControlParametersCommandFormat,
+         typename ResponseFormatType = ControlParametersCommandFormat>
+class ControlCommand : noncopyable
+{
+protected:
+  using Base = ControlCommand<Derived, RequestFormatType, ResponseFormatType>;
+
+public:
+  using RequestFormat = RequestFormatType;
+  using ResponseFormat = ResponseFormatType;
+  using RequestParameters = typename RequestFormat::ParametersType;
+  using ResponseParameters = typename ResponseFormat::ParametersType;
+
+  ControlCommand() = delete;
+
+  /**
+   * \brief Construct request Interest.
+   * \throw ArgumentError if parameters are invalid
+   */
+  static Interest
+  createRequest(Name commandPrefix, const RequestParameters& params)
+  {
+    validateRequest(params);
+
+    Interest request(commandPrefix.append(Derived::s_module).append(Derived::s_verb));
+    Derived::s_requestFormat.encode(request, params);
+    return request;
+  }
+
+  /**
+   * \brief Validate request parameters.
+   * \throw ArgumentError if parameters are invalid
+   */
+  static void
+  validateRequest(const RequestParameters& params)
+  {
+    Derived::s_requestFormat.validate(params);
+    Derived::validateRequestImpl(params);
+  }
+
+  /**
+   * \brief Apply default values to missing fields in request.
+   */
+  static void
+  applyDefaultsToRequest(RequestParameters& params)
+  {
+    Derived::applyDefaultsToRequestImpl(params);
+  }
+
+  /**
+   * \brief Validate response parameters.
+   * \throw ArgumentError if parameters are invalid
+   */
+  static void
+  validateResponse(const ResponseParameters& params)
+  {
+    Derived::s_responseFormat.validate(params);
+    Derived::validateResponseImpl(params);
+  }
+
+  /**
+   * \brief Apply default values to missing fields in response.
+   */
+  static void
+  applyDefaultsToResponse(ResponseParameters& params)
+  {
+    Derived::applyDefaultsToResponseImpl(params);
+  }
+
+private:
+  static void
+  validateRequestImpl(const RequestParameters&)
+  {
+  }
+
+  static void
+  applyDefaultsToRequestImpl(RequestParameters&)
+  {
+  }
+
+  static void
+  validateResponseImpl(const ResponseParameters&)
+  {
+  }
+
+  static void
+  applyDefaultsToResponseImpl(ResponseParameters&)
+  {
+  }
+};
+
+#define NDN_CXX_CONTROL_COMMAND(cmd, module, verb) \
+  private: \
+  friend Base; \
+  static inline const ::ndn::name::Component s_module{module}; \
+  static inline const ::ndn::name::Component s_verb{verb}; \
+  static const RequestFormat s_requestFormat; \
+  static const ResponseFormat s_responseFormat
+
+
+/**
+ * \ingroup management
  * \brief Represents a `faces/create` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/FaceMgmt#Create-a-face
  */
-class FaceCreateCommand : public ControlCommand
+class FaceCreateCommand : public ControlCommand<FaceCreateCommand>
 {
-public:
-  FaceCreateCommand();
+  NDN_CXX_CONTROL_COMMAND(FaceCreateCommand, "faces", "create");
 
-  void
-  applyDefaultsToRequest(ControlParameters& parameters) const override;
+  static void
+  applyDefaultsToRequestImpl(ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -152,20 +222,19 @@
  * \brief Represents a `faces/update` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/FaceMgmt#Update-the-static-properties-of-a-face
  */
-class FaceUpdateCommand : public ControlCommand
+class FaceUpdateCommand : public ControlCommand<FaceUpdateCommand>
 {
-public:
-  FaceUpdateCommand();
+  NDN_CXX_CONTROL_COMMAND(FaceUpdateCommand, "faces", "update");
 
-  void
-  applyDefaultsToRequest(ControlParameters& parameters) const override;
+  static void
+  applyDefaultsToRequestImpl(ControlParameters& parameters);
 
   /**
    * \note This can only validate ControlParameters in a success response.
    *       Failure responses should be validated with validateRequest.
    */
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -174,16 +243,15 @@
  * \brief Represents a `faces/destroy` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/FaceMgmt#Destroy-a-face
  */
-class FaceDestroyCommand : public ControlCommand
+class FaceDestroyCommand : public ControlCommand<FaceDestroyCommand>
 {
-public:
-  FaceDestroyCommand();
+  NDN_CXX_CONTROL_COMMAND(FaceDestroyCommand, "faces", "destroy");
 
-  void
-  validateRequest(const ControlParameters& parameters) const override;
+  static void
+  validateRequestImpl(const ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -192,16 +260,15 @@
  * \brief Represents a `fib/add-nexthop` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/FibMgmt#Add-a-nexthop
  */
-class FibAddNextHopCommand : public ControlCommand
+class FibAddNextHopCommand : public ControlCommand<FibAddNextHopCommand>
 {
-public:
-  FibAddNextHopCommand();
+  NDN_CXX_CONTROL_COMMAND(FibAddNextHopCommand, "fib", "add-nexthop");
 
-  void
-  applyDefaultsToRequest(ControlParameters& parameters) const override;
+  static void
+  applyDefaultsToRequestImpl(ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -210,16 +277,15 @@
  * \brief Represents a `fib/remove-nexthop` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/FibMgmt#Remove-a-nexthop
  */
-class FibRemoveNextHopCommand : public ControlCommand
+class FibRemoveNextHopCommand : public ControlCommand<FibRemoveNextHopCommand>
 {
-public:
-  FibRemoveNextHopCommand();
+  NDN_CXX_CONTROL_COMMAND(FibRemoveNextHopCommand, "fib", "remove-nexthop");
 
-  void
-  applyDefaultsToRequest(ControlParameters& parameters) const override;
+  static void
+  applyDefaultsToRequestImpl(ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -228,10 +294,9 @@
  * \brief Represents a `cs/config` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/CsMgmt#Update-configuration
  */
-class CsConfigCommand : public ControlCommand
+class CsConfigCommand : public ControlCommand<CsConfigCommand>
 {
-public:
-  CsConfigCommand();
+  NDN_CXX_CONTROL_COMMAND(CsConfigCommand, "cs", "config");
 };
 
 
@@ -240,16 +305,15 @@
  * \brief Represents a `cs/erase` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/CsMgmt#Erase-entries
  */
-class CsEraseCommand : public ControlCommand
+class CsEraseCommand : public ControlCommand<CsEraseCommand>
 {
-public:
-  CsEraseCommand();
+  NDN_CXX_CONTROL_COMMAND(CsEraseCommand, "cs", "erase");
 
-  void
-  validateRequest(const ControlParameters& parameters) const override;
+  static void
+  validateRequestImpl(const ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -258,28 +322,26 @@
  * \brief Represents a `strategy-choice/set` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/StrategyChoice#Set-the-strategy-for-a-namespace
  */
-class StrategyChoiceSetCommand : public ControlCommand
+class StrategyChoiceSetCommand : public ControlCommand<StrategyChoiceSetCommand>
 {
-public:
-  StrategyChoiceSetCommand();
+  NDN_CXX_CONTROL_COMMAND(StrategyChoiceSetCommand, "strategy-choice", "set");
 };
 
 
 /**
  * \ingroup management
- * \brief Represents a `strategy-choice/set` command.
+ * \brief Represents a `strategy-choice/unset` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/StrategyChoice#Unset-the-strategy-for-a-namespace
  */
-class StrategyChoiceUnsetCommand : public ControlCommand
+class StrategyChoiceUnsetCommand : public ControlCommand<StrategyChoiceUnsetCommand>
 {
-public:
-  StrategyChoiceUnsetCommand();
+  NDN_CXX_CONTROL_COMMAND(StrategyChoiceUnsetCommand, "strategy-choice", "unset");
 
-  void
-  validateRequest(const ControlParameters& parameters) const override;
+  static void
+  validateRequestImpl(const ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -288,16 +350,15 @@
  * \brief Represents a `rib/register` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/RibMgmt#Register-a-route
  */
-class RibRegisterCommand : public ControlCommand
+class RibRegisterCommand : public ControlCommand<RibRegisterCommand>
 {
-public:
-  RibRegisterCommand();
+  NDN_CXX_CONTROL_COMMAND(RibRegisterCommand, "rib", "register");
 
-  void
-  applyDefaultsToRequest(ControlParameters& parameters) const override;
+  static void
+  applyDefaultsToRequestImpl(ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 
@@ -306,16 +367,15 @@
  * \brief Represents a `rib/unregister` command.
  * \sa https://redmine.named-data.net/projects/nfd/wiki/RibMgmt#Unregister-a-route
  */
-class RibUnregisterCommand : public ControlCommand
+class RibUnregisterCommand : public ControlCommand<RibUnregisterCommand>
 {
-public:
-  RibUnregisterCommand();
+  NDN_CXX_CONTROL_COMMAND(RibUnregisterCommand, "rib", "unregister");
 
-  void
-  applyDefaultsToRequest(ControlParameters& parameters) const override;
+  static void
+  applyDefaultsToRequestImpl(ControlParameters& parameters);
 
-  void
-  validateResponse(const ControlParameters& parameters) const override;
+  static void
+  validateResponseImpl(const ControlParameters& parameters);
 };
 
 } // namespace ndn::nfd
diff --git a/ndn-cxx/mgmt/nfd/controller.cpp b/ndn-cxx/mgmt/nfd/controller.cpp
index 4cc2fa8..f0dd41b 100644
--- a/ndn-cxx/mgmt/nfd/controller.cpp
+++ b/ndn-cxx/mgmt/nfd/controller.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -43,27 +43,26 @@
 }
 
 void
-Controller::startCommand(const shared_ptr<ControlCommand>& command,
-                         const ControlParameters& parameters,
-                         const CommandSuccessCallback& onSuccess,
-                         const CommandFailureCallback& onFailure,
-                         const CommandOptions& options)
+Controller::sendCommandRequest(Interest& interest,
+                               const security::SigningInfo& signingInfo,
+                               ResponseParametersValidator checkResponse,
+                               CommandSuccessCallback onSuccess,
+                               const CommandFailureCallback& onFailure)
 {
-  Interest interest;
-  interest.setName(command->getRequestName(options.getPrefix(), parameters));
-  interest.setInterestLifetime(options.getTimeout());
-  m_signer.makeSignedInterest(interest, options.getSigningInfo());
+  BOOST_ASSERT(checkResponse);
+
+  m_signer.makeSignedInterest(interest, signingInfo);
 
   m_face.expressInterest(interest,
-    [=] (const Interest&, const Data& data) {
-      processCommandResponse(data, command, onSuccess, onFailure);
+    [=, check = std::move(checkResponse), success = std::move(onSuccess)] (const auto&, const Data& d) {
+      processCommandResponse(d, std::move(check), std::move(success), onFailure);
     },
-    [=] (const Interest&, const lp::Nack& nack) {
+    [onFailure] (const Interest&, const lp::Nack& nack) {
       if (onFailure)
         onFailure(ControlResponse(Controller::ERROR_NACK,
                                   "received Nack: " + boost::lexical_cast<std::string>(nack.getReason())));
     },
-    [=] (const Interest&) {
+    [onFailure] (const Interest&) {
       if (onFailure)
         onFailure(ControlResponse(Controller::ERROR_TIMEOUT, "request timed out"));
     });
@@ -71,15 +70,15 @@
 
 void
 Controller::processCommandResponse(const Data& data,
-                                   const shared_ptr<ControlCommand>& command,
-                                   const CommandSuccessCallback& onSuccess,
+                                   ResponseParametersValidator checkResponse,
+                                   CommandSuccessCallback onSuccess,
                                    const CommandFailureCallback& onFailure)
 {
   m_validator.validate(data,
-    [=] (const Data& d) {
-      processValidatedCommandResponse(d, command, onSuccess, onFailure);
+    [check = std::move(checkResponse), success = std::move(onSuccess), onFailure] (const Data& d) {
+      processValidatedCommandResponse(d, check, success, onFailure);
     },
-    [=] (const Data&, const auto& error) {
+    [onFailure] (const Data&, const auto& error) {
       if (onFailure)
         onFailure(ControlResponse(ERROR_VALIDATION, boost::lexical_cast<std::string>(error)));
     }
@@ -88,7 +87,7 @@
 
 void
 Controller::processValidatedCommandResponse(const Data& data,
-                                            const shared_ptr<ControlCommand>& command,
+                                            const ResponseParametersValidator& checkResponse,
                                             const CommandSuccessCallback& onSuccess,
                                             const CommandFailureCallback& onFailure)
 {
@@ -119,9 +118,9 @@
   }
 
   try {
-    command->validateResponse(parameters);
+    checkResponse(parameters);
   }
-  catch (const ControlCommand::ArgumentError& e) {
+  catch (const std::invalid_argument& e) {
     if (onFailure)
       onFailure(ControlResponse(ERROR_SERVER, "Invalid response: "s + e.what()));
     return;
diff --git a/ndn-cxx/mgmt/nfd/controller.hpp b/ndn-cxx/mgmt/nfd/controller.hpp
index b9adc75..e149627 100644
--- a/ndn-cxx/mgmt/nfd/controller.hpp
+++ b/ndn-cxx/mgmt/nfd/controller.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -23,7 +23,7 @@
 #define NDN_CXX_MGMT_NFD_CONTROLLER_HPP
 
 #include "ndn-cxx/mgmt/nfd/command-options.hpp"
-#include "ndn-cxx/mgmt/nfd/control-command.hpp"
+#include "ndn-cxx/mgmt/nfd/control-parameters.hpp"
 #include "ndn-cxx/mgmt/nfd/control-response.hpp"
 #include "ndn-cxx/security/interest-signer.hpp"
 #include "ndn-cxx/security/key-chain.hpp"
@@ -89,14 +89,19 @@
   /**
    * \brief Start command execution.
    */
-  template<typename Command>
+  template<typename Command,
+           typename CommandParameters = typename Command::RequestParameters>
   void
-  start(const ControlParameters& parameters,
-        const CommandSuccessCallback& onSuccess,
+  start(const CommandParameters& parameters,
+        CommandSuccessCallback onSuccess,
         const CommandFailureCallback& onFailure,
         const CommandOptions& options = {})
   {
-    startCommand(std::make_shared<Command>(), parameters, onSuccess, onFailure, options);
+    auto request = Command::createRequest(options.getPrefix(), parameters);
+    request.setInterestLifetime(options.getTimeout());
+    sendCommandRequest(request, options.getSigningInfo(),
+                       [] (const auto& responseParams) { Command::validateResponse(responseParams); },
+                       std::move(onSuccess), onFailure);
   }
 
   /**
@@ -125,22 +130,24 @@
   }
 
 private:
+  using ResponseParametersValidator = std::function<void(const ControlParameters&)>;
+
   void
-  startCommand(const shared_ptr<ControlCommand>& command,
-               const ControlParameters& parameters,
-               const CommandSuccessCallback& onSuccess,
-               const CommandFailureCallback& onFailure,
-               const CommandOptions& options);
+  sendCommandRequest(Interest& interest,
+                     const security::SigningInfo& signingInfo,
+                     ResponseParametersValidator checkResponse,
+                     CommandSuccessCallback onSuccess,
+                     const CommandFailureCallback& onFailure);
 
   void
   processCommandResponse(const Data& data,
-                         const shared_ptr<ControlCommand>& command,
-                         const CommandSuccessCallback& onSuccess,
+                         ResponseParametersValidator checkResponse,
+                         CommandSuccessCallback onSuccess,
                          const CommandFailureCallback& onFailure);
 
   static void
   processValidatedCommandResponse(const Data& data,
-                                  const shared_ptr<ControlCommand>& command,
+                                  const ResponseParametersValidator& checkResponse,
                                   const CommandSuccessCallback& onSuccess,
                                   const CommandFailureCallback& onFailure);
 
diff --git a/tests/unit/mgmt/nfd/control-command.t.cpp b/tests/unit/mgmt/nfd/control-command.t.cpp
index 2da5270..dc8cff0 100644
--- a/tests/unit/mgmt/nfd/control-command.t.cpp
+++ b/tests/unit/mgmt/nfd/control-command.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -33,13 +33,14 @@
 
 BOOST_AUTO_TEST_CASE(FaceCreateRequest)
 {
-  FaceCreateCommand command;
+  using Command = FaceCreateCommand;
 
   // good with required fields only
   ControlParameters p1;
   p1.setUri("tcp4://192.0.2.1:6363");
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK(Name("/PREFIX/faces/create").isPrefixOf(command.getRequestName("/PREFIX", p1)));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
+  BOOST_CHECK(Name("/PREFIX/faces/create").isPrefixOf(n1));
 
   // good with optional fields
   ControlParameters p2(p1);
@@ -50,31 +51,31 @@
     .setMtu(8192)
     .setFlags(0x3)
     .setMask(0x1);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
 
   // Uri is required
   ControlParameters p3;
-  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p3), ArgumentError);
 
   // Name is forbidden
   ControlParameters p4(p1);
   p4.setName("/example");
-  BOOST_CHECK_THROW(command.validateRequest(p4), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p4), ArgumentError);
 
   // Flags and Mask must be specified together
   ControlParameters p5(p1);
   p5.setFlags(0x3);
-  BOOST_CHECK_THROW(command.validateRequest(p5), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p5), ArgumentError);
 
   // Flags and Mask must be specified together
   ControlParameters p6(p1);
   p6.setMask(0x1);
-  BOOST_CHECK_THROW(command.validateRequest(p6), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p6), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(FaceCreateResponse)
 {
-  FaceCreateCommand command;
+  using Command = FaceCreateCommand;
 
   // good
   ControlParameters p1;
@@ -86,45 +87,45 @@
     .setDefaultCongestionThreshold(12345)
     .setMtu(2048)
     .setFlags(0x3);
-  BOOST_CHECK_NO_THROW(command.validateResponse(p1));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p1));
 
   // Name is forbidden
   ControlParameters p2(p1);
   p2.setName("/example");
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // Mask is forbidden
   ControlParameters p3(p1);
   p3.setMask(0x1);
-  BOOST_CHECK_THROW(command.validateResponse(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p3), ArgumentError);
 
   // FaceId must be valid
   ControlParameters p4(p1);
   p4.setFaceId(INVALID_FACE_ID);
-  BOOST_CHECK_THROW(command.validateResponse(p4), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p4), ArgumentError);
 
   // LocalUri is required
   ControlParameters p5(p1);
   p5.unsetLocalUri();
-  BOOST_CHECK_THROW(command.validateResponse(p5), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p5), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(FaceUpdate)
 {
-  FaceUpdateCommand command;
+  using Command = FaceUpdateCommand;
 
   // FaceId must be valid
   ControlParameters p1;
   p1.setFaceId(0);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
   p1.setFaceId(INVALID_FACE_ID);
-  BOOST_CHECK_THROW(command.validateResponse(p1), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p1), ArgumentError);
 
   // Persistency and Flags are required in response
   p1.setFaceId(1);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_THROW(command.validateResponse(p1), ControlCommand::ArgumentError);
-  command.applyDefaultsToRequest(p1);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_THROW(Command::validateResponse(p1), ArgumentError);
+  Command::applyDefaultsToRequest(p1);
   BOOST_CHECK_EQUAL(p1.getFaceId(), 1);
 
   // Good request, bad response (Mask is forbidden but present)
@@ -135,86 +136,84 @@
     .setDefaultCongestionThreshold(54321)
     .setMtu(8192)
     .setFlagBit(BIT_LOCAL_FIELDS_ENABLED, false);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // Flags without Mask (good response, bad request)
   p2.unsetMask();
-  BOOST_CHECK_THROW(command.validateRequest(p2), ControlCommand::ArgumentError);
-  BOOST_CHECK_NO_THROW(command.validateResponse(p2));
+  BOOST_CHECK_THROW(Command::validateRequest(p2), ArgumentError);
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p2));
 
   // FaceId is optional in request
   p2.setFlagBit(BIT_LOCAL_FIELDS_ENABLED, false);
   p2.unsetFaceId();
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
 
   // Name is forbidden
   ControlParameters p3;
   p3.setFaceId(1)
     .setName("/ndn/name");
-  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p3), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p3), ArgumentError);
 
   // Uri is forbidden
   ControlParameters p4;
   p4.setFaceId(1)
     .setUri("tcp4://192.0.2.1");
-  BOOST_CHECK_THROW(command.validateRequest(p4), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p4), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p4), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p4), ArgumentError);
 
   // Empty request is valid, empty response is invalid
   ControlParameters p5;
-  BOOST_CHECK_NO_THROW(command.validateRequest(p5));
-  BOOST_CHECK_THROW(command.validateResponse(p5), ControlCommand::ArgumentError);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p5));
+  BOOST_CHECK_THROW(Command::validateResponse(p5), ArgumentError);
   BOOST_CHECK(!p5.hasFaceId());
 
   // Default request, not valid response due to missing FacePersistency and Flags
-  command.applyDefaultsToRequest(p5);
+  Command::applyDefaultsToRequest(p5);
   BOOST_REQUIRE(p5.hasFaceId());
-  BOOST_CHECK_NO_THROW(command.validateRequest(p5));
-  BOOST_CHECK_THROW(command.validateResponse(p5), ControlCommand::ArgumentError);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p5));
+  BOOST_CHECK_THROW(Command::validateResponse(p5), ArgumentError);
   BOOST_CHECK_EQUAL(p5.getFaceId(), 0);
 }
 
 BOOST_AUTO_TEST_CASE(FaceDestroy)
 {
-  FaceDestroyCommand command;
+  using Command = FaceDestroyCommand;
 
   // Uri is forbidden
   ControlParameters p1;
   p1.setUri("tcp4://192.0.2.1")
     .setFaceId(4);
-  BOOST_CHECK_THROW(command.validateRequest(p1), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p1), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p1), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p1), ArgumentError);
 
   // FaceId must be valid
   ControlParameters p2;
   p2.setFaceId(INVALID_FACE_ID);
-  BOOST_CHECK_THROW(command.validateRequest(p2), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p2), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // Good request, good response
   ControlParameters p3;
   p3.setFaceId(6);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p3));
-  BOOST_CHECK_NO_THROW(command.validateResponse(p3));
-  Name n3;
-  BOOST_CHECK_NO_THROW(n3 = command.getRequestName("/PREFIX", p3));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p3));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p3));
+  Name n3 = Command::createRequest("/PREFIX", p3).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/faces/destroy").isPrefixOf(n3));
 }
 
 BOOST_AUTO_TEST_CASE(FibAddNextHop)
 {
-  FibAddNextHopCommand command;
+  using Command = FibAddNextHopCommand;
 
   // Cost required in response
   ControlParameters p1;
   p1.setName("ndn:/")
     .setFaceId(22);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_THROW(command.validateResponse(p1), ControlCommand::ArgumentError);
-  Name n1;
-  BOOST_CHECK_NO_THROW(n1 = command.getRequestName("/PREFIX", p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_THROW(Command::validateResponse(p1), ArgumentError);
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/fib/add-nexthop").isPrefixOf(n1));
 
   // Good request, bad response (FaceId must be valid)
@@ -222,231 +221,229 @@
   p2.setName("ndn:/example")
     .setFaceId(0)
     .setCost(6);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
   p2.setFaceId(INVALID_FACE_ID);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // Default request
-  command.applyDefaultsToRequest(p1);
+  Command::applyDefaultsToRequest(p1);
   BOOST_REQUIRE(p1.hasCost());
   BOOST_CHECK_EQUAL(p1.getCost(), 0);
 
   // FaceId optional in request
   p1.unsetFaceId();
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  command.applyDefaultsToRequest(p1);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  Command::applyDefaultsToRequest(p1);
   BOOST_REQUIRE(p1.hasFaceId());
   BOOST_CHECK_EQUAL(p1.getFaceId(), 0);
 }
 
 BOOST_AUTO_TEST_CASE(FibRemoveNextHop)
 {
-  FibRemoveNextHopCommand command;
+  using Command = FibRemoveNextHopCommand;
 
   // Good request, good response
   ControlParameters p1;
   p1.setName("ndn:/")
     .setFaceId(22);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_NO_THROW(command.validateResponse(p1));
-  Name n1;
-  BOOST_CHECK_NO_THROW(n1 = command.getRequestName("/PREFIX", p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p1));
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/fib/remove-nexthop").isPrefixOf(n1));
 
   // Good request, bad response (FaceId must be valid)
   ControlParameters p2;
   p2.setName("ndn:/example")
     .setFaceId(0);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
   p2.setFaceId(INVALID_FACE_ID);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // FaceId is optional in request
   p1.unsetFaceId();
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  command.applyDefaultsToRequest(p1);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  Command::applyDefaultsToRequest(p1);
   BOOST_REQUIRE(p1.hasFaceId());
   BOOST_CHECK_EQUAL(p1.getFaceId(), 0);
 }
 
 BOOST_AUTO_TEST_CASE(CsConfigRequest)
 {
-  CsConfigCommand command;
+  using Command = CsConfigCommand;
 
   // good empty request
   ControlParameters p1;
-  command.validateRequest(p1);
-  BOOST_CHECK(Name("/PREFIX/cs/config").isPrefixOf(command.getRequestName("/PREFIX", p1)));
+  Command::validateRequest(p1);
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
+  BOOST_CHECK(Name("/PREFIX/cs/config").isPrefixOf(n1));
 
   // good full request
   ControlParameters p2;
   p2.setCapacity(1574);
   p2.setFlagBit(BIT_CS_ENABLE_ADMIT, true);
   p2.setFlagBit(BIT_CS_ENABLE_SERVE, true);
-  command.validateRequest(p2);
+  Command::validateRequest(p2);
 
   // bad request: Flags but no Mask
   ControlParameters p3(p2);
   p3.unsetMask();
-  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p3), ArgumentError);
 
   // bad request: Mask but no Flags
   ControlParameters p4(p2);
   p4.unsetFlags();
-  BOOST_CHECK_THROW(command.validateRequest(p4), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p4), ArgumentError);
 
   // bad request: forbidden field
   ControlParameters p5(p2);
   p5.setName("/example");
-  BOOST_CHECK_THROW(command.validateRequest(p5), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p5), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(CsConfigResponse)
 {
-  CsConfigCommand command;
+  using Command = CsConfigCommand;
 
   // bad empty response
   ControlParameters p1;
-  BOOST_CHECK_THROW(command.validateResponse(p1), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p1), ArgumentError);
 
   // bad response: Mask not allowed
   ControlParameters p2;
   p2.setCapacity(1574);
   p2.setFlagBit(BIT_CS_ENABLE_ADMIT, true);
   p2.setFlagBit(BIT_CS_ENABLE_SERVE, true);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // good response
   ControlParameters p3(p2);
   p3.unsetMask();
-  command.validateResponse(p3);
+  Command::validateResponse(p3);
 }
 
 BOOST_AUTO_TEST_CASE(CsEraseRequest)
 {
-  CsEraseCommand command;
+  using Command = CsEraseCommand;
 
   // good no-limit request
   ControlParameters p1;
   p1.setName("/u4LYPNU8Q");
-  command.validateRequest(p1);
-  BOOST_CHECK(Name("/PREFIX/cs/erase").isPrefixOf(command.getRequestName("/PREFIX", p1)));
+  Command::validateRequest(p1);
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
+  BOOST_CHECK(Name("/PREFIX/cs/erase").isPrefixOf(n1));
 
   // good limited request
   ControlParameters p2;
   p2.setName("/IMw1RaLF");
   p2.setCount(177);
-  command.validateRequest(p2);
+  Command::validateRequest(p2);
 
   // bad request: zero entry
   ControlParameters p3;
   p3.setName("/ahMID1jcib");
   p3.setCount(0);
-  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p3), ArgumentError);
 
   // bad request: forbidden field
   ControlParameters p4(p2);
   p4.setCapacity(278);
-  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p3), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(CsEraseResponse)
 {
-  CsEraseCommand command;
+  using Command = CsEraseCommand;
 
   // good normal response
   ControlParameters p1;
   p1.setName("/TwiIwCdR");
   p1.setCount(1);
-  command.validateResponse(p1);
+  Command::validateResponse(p1);
 
   // good limit exceeded request
   ControlParameters p2;
   p2.setName("/NMsiy44pr");
   p2.setCapacity(360);
   p2.setCount(360);
-  command.validateResponse(p2);
+  Command::validateResponse(p2);
 
   // good zero-entry response
   ControlParameters p3;
   p3.setName("/5f1LRPh1L");
   p3.setCount(0);
-  command.validateResponse(p3);
+  Command::validateResponse(p3);
 
   // bad request: missing Count
   ControlParameters p4(p1);
   p4.unsetCount();
-  BOOST_CHECK_THROW(command.validateResponse(p4), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p4), ArgumentError);
 
   // bad request: zero capacity
   ControlParameters p5(p1);
   p5.setCapacity(0);
-  BOOST_CHECK_THROW(command.validateResponse(p5), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p5), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(StrategyChoiceSet)
 {
-  StrategyChoiceSetCommand command;
+  using Command = StrategyChoiceSetCommand;
 
   // Good request, good response
   ControlParameters p1;
   p1.setName("ndn:/")
     .setStrategy("ndn:/strategy/P");
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_NO_THROW(command.validateResponse(p1));
-  Name n1;
-  BOOST_CHECK_NO_THROW(n1 = command.getRequestName("/PREFIX", p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p1));
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/strategy-choice/set").isPrefixOf(n1));
 
   // Strategy is required in both requests and responses
   ControlParameters p2;
   p2.setName("ndn:/example");
-  BOOST_CHECK_THROW(command.validateRequest(p2), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p2), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(StrategyChoiceUnset)
 {
-  StrategyChoiceUnsetCommand command;
+  using Command = StrategyChoiceUnsetCommand;
 
   // Good request, good response
   ControlParameters p1;
   p1.setName("ndn:/example");
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_NO_THROW(command.validateResponse(p1));
-  Name n1;
-  BOOST_CHECK_NO_THROW(n1 = command.getRequestName("/PREFIX", p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p1));
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/strategy-choice/unset").isPrefixOf(n1));
 
   // Strategy is forbidden
   ControlParameters p2;
   p2.setName("ndn:/example")
     .setStrategy("ndn:/strategy/P");
-  BOOST_CHECK_THROW(command.validateRequest(p2), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p2), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // Name must have at least one component
   ControlParameters p3;
   p3.setName("ndn:/");
-  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
-  BOOST_CHECK_THROW(command.validateResponse(p3), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateRequest(p3), ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p3), ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(RibRegister)
 {
-  RibRegisterCommand command;
+  using Command = RibRegisterCommand;
 
   // Good request, response missing many fields
   ControlParameters p1;
   p1.setName("ndn:/");
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_THROW(command.validateResponse(p1), ControlCommand::ArgumentError);
-  Name n1;
-  BOOST_CHECK_NO_THROW(n1 = command.getRequestName("/PREFIX", p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_THROW(Command::validateResponse(p1), ArgumentError);
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/rib/register").isPrefixOf(n1));
 
   // Default request
-  command.applyDefaultsToRequest(p1);
+  Command::applyDefaultsToRequest(p1);
   BOOST_REQUIRE(p1.hasOrigin());
   BOOST_CHECK_EQUAL(p1.getOrigin(), ROUTE_ORIGIN_APP);
   BOOST_REQUIRE(p1.hasCost());
@@ -460,25 +457,24 @@
   p2.setName("ndn:/example")
     .setFaceId(2)
     .setCost(6);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
-  command.applyDefaultsToRequest(p2);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
+  Command::applyDefaultsToRequest(p2);
   BOOST_CHECK_EQUAL(p2.hasExpirationPeriod(), false);
-  BOOST_CHECK_NO_THROW(command.validateResponse(p2));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p2));
 }
 
 BOOST_AUTO_TEST_CASE(RibUnregister)
 {
-  RibUnregisterCommand command;
+  using Command = RibUnregisterCommand;
 
   // Good request, good response
   ControlParameters p1;
   p1.setName("ndn:/")
     .setFaceId(22)
     .setOrigin(ROUTE_ORIGIN_STATIC);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p1));
-  BOOST_CHECK_NO_THROW(command.validateResponse(p1));
-  Name n1;
-  BOOST_CHECK_NO_THROW(n1 = command.getRequestName("/PREFIX", p1));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p1));
+  BOOST_CHECK_NO_THROW(Command::validateResponse(p1));
+  Name n1 = Command::createRequest("/PREFIX", p1).getName();
   BOOST_CHECK(Name("ndn:/PREFIX/rib/unregister").isPrefixOf(n1));
 
   // Good request, bad response (FaceId must be valid)
@@ -486,14 +482,14 @@
   p2.setName("ndn:/example")
     .setFaceId(0)
     .setOrigin(ROUTE_ORIGIN_APP);
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
   p2.setFaceId(INVALID_FACE_ID);
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 
   // FaceId is optional in request, required in response
   p2.unsetFaceId();
-  BOOST_CHECK_NO_THROW(command.validateRequest(p2));
-  BOOST_CHECK_THROW(command.validateResponse(p2), ControlCommand::ArgumentError);
+  BOOST_CHECK_NO_THROW(Command::validateRequest(p2));
+  BOOST_CHECK_THROW(Command::validateResponse(p2), ArgumentError);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestControlCommand
diff --git a/tests/unit/mgmt/nfd/controller.t.cpp b/tests/unit/mgmt/nfd/controller.t.cpp
index 6760d5e..8be9d23 100644
--- a/tests/unit/mgmt/nfd/controller.t.cpp
+++ b/tests/unit/mgmt/nfd/controller.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -20,6 +20,7 @@
  */
 
 #include "ndn-cxx/mgmt/nfd/controller.hpp"
+#include "ndn-cxx/mgmt/nfd/control-command.hpp"
 #include "ndn-cxx/mgmt/nfd/control-response.hpp"
 
 #include "tests/test-common.hpp"
@@ -87,8 +88,7 @@
   // 6 components: /localhost/nfd/faces/create/<parameters>/params-sha256=...
   BOOST_REQUIRE_EQUAL(requestInterest.getName().size(), 6);
   ControlParameters requestParams(requestInterest.getName()[4].blockFromValue());
-  FaceCreateCommand command;
-  BOOST_CHECK_NO_THROW(command.validateRequest(requestParams));
+  BOOST_CHECK_NO_THROW(FaceCreateCommand::validateRequest(requestParams));
   BOOST_CHECK_EQUAL(requestParams.getUri(), parameters.getUri());
 
   ControlParameters responseBody = makeFaceCreateResponse();
@@ -131,12 +131,11 @@
   this->advanceClocks(1_ms);
 
   BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
-  const Interest& requestInterest = face.sentInterests[0];
+  const Interest& request = face.sentInterests[0];
 
-  FaceCreateCommand command;
-  BOOST_CHECK(Name("/localhop/net/example/router1/nfd/rib/register").isPrefixOf(requestInterest.getName()));
-  BOOST_CHECK(requestInterest.isSigned());
-  BOOST_CHECK(requestInterest.isParametersDigestValid());
+  BOOST_CHECK(Name("/localhop/net/example/router1/nfd/rib/register").isPrefixOf(request.getName()));
+  BOOST_CHECK(request.isSigned());
+  BOOST_CHECK(request.isParametersDigestValid());
 }
 
 BOOST_AUTO_TEST_CASE(InvalidRequest)
@@ -146,7 +145,7 @@
   // Uri is missing
 
   BOOST_CHECK_THROW(controller.start<FaceCreateCommand>(parameters, succeedCallback, commandFailCallback),
-                    ControlCommand::ArgumentError);
+                    ArgumentError);
 }
 
 BOOST_AUTO_TEST_CASE(ValidationFailure)
diff --git a/tests/unit/util/dummy-client-face.t.cpp b/tests/unit/util/dummy-client-face.t.cpp
index a946858..8f40f4c 100644
--- a/tests/unit/util/dummy-client-face.t.cpp
+++ b/tests/unit/util/dummy-client-face.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2024 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -20,6 +20,7 @@
  */
 
 #include "ndn-cxx/util/dummy-client-face.hpp"
+#include "ndn-cxx/mgmt/nfd/control-command.hpp"
 #include "ndn-cxx/mgmt/nfd/controller.hpp"
 
 #include "tests/test-common.hpp"