Avoid deprecated Boost.Asio interfaces

Change-Id: I00d285893ff61619f49dff8a8a55d0d0e2c309a7
diff --git a/.waf-tools/default-compiler-flags.py b/.waf-tools/default-compiler-flags.py
index 4aa9e9b..bee5072 100644
--- a/.waf-tools/default-compiler-flags.py
+++ b/.waf-tools/default-compiler-flags.py
@@ -128,7 +128,11 @@
 
     def getGeneralFlags(self, conf):
         """Get dict of CXXFLAGS, LINKFLAGS, and DEFINES that are always needed"""
-        return {'CXXFLAGS': [], 'LINKFLAGS': [], 'DEFINES': []}
+        return {
+            'CXXFLAGS': [],
+            'LINKFLAGS': [],
+            'DEFINES': ['BOOST_ASIO_NO_DEPRECATED', 'BOOST_FILESYSTEM_NO_DEPRECATED'],
+        }
 
     def getDebugFlags(self, conf):
         """Get dict of CXXFLAGS, LINKFLAGS, and DEFINES that are needed only in debug mode"""
diff --git a/docs/examples.rst b/docs/examples.rst
index a2a0a0c..cdc2df2 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -49,13 +49,13 @@
 The following example demonstrates how to use :ndn-cxx:`Scheduler` to schedule arbitrary
 events for execution at specific points of time.
 
-The library internally uses `boost::asio::io_service
-<https://www.boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/io_service.html>`_ to
-implement fully asynchronous NDN operations (i.e., sending and receiving Interests and
-Data).  In addition to network-related operations, ``boost::asio::io_service`` can be used
+The library internally uses `boost::asio::io_context
+<https://www.boost.org/doc/libs/1_71_0/doc/html/boost_asio/reference/io_context.html>`__
+to implement fully asynchronous NDN operations (i.e., sending and receiving Interests and
+Data).  In addition to network-related operations, ``boost::asio::io_context`` can be used
 to execute any arbitrary callback within the processing thread (run either explicitly via
-``io_service::run()`` or implicitly via :ndn-cxx:`Face::processEvents` as in previous
-examples). :ndn-cxx:`Scheduler` is just a wrapper on top of ``io_service``, providing a
+``io_context::run()`` or implicitly via :ndn-cxx:`Face::processEvents` as in previous
+examples). :ndn-cxx:`Scheduler` is just a wrapper on top of ``io_context``, providing a
 simple interface to schedule tasks at specific times.
 
 The highlighted lines in the example demonstrate all that is needed to express a second
diff --git a/docs/release-notes/release-notes-0.2.0.rst b/docs/release-notes/release-notes-0.2.0.rst
index 89d7c08..52bb4e4 100644
--- a/docs/release-notes/release-notes-0.2.0.rst
+++ b/docs/release-notes/release-notes-0.2.0.rst
@@ -145,7 +145,7 @@
 
   Use versions that accept reference to ``io_service`` object.
 
-- ``Face::ioService`` method, use :ndn-cxx:`Face::getIoService` instead.
+- ``Face::ioService`` method, use ``Face::getIoService`` instead.
 
 - :ndn-cxx:`Interest` constructor that accepts name, individual selectors, and individual
   guiders as constructor parameters.
diff --git a/examples/consumer-with-timer.cpp b/examples/consumer-with-timer.cpp
index 87abfa9..32a14eb 100644
--- a/examples/consumer-with-timer.cpp
+++ b/examples/consumer-with-timer.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2022 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -22,7 +22,7 @@
 #include <ndn-cxx/face.hpp>
 #include <ndn-cxx/util/scheduler.hpp>
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 #include <iostream>
 
 // Enclosing code in ndn simplifies coding (can also use `using namespace ndn`)
@@ -52,8 +52,8 @@
     // Schedule a new event
     m_scheduler.schedule(3_s, [this] { delayedInterest(); });
 
-    // m_ioService.run() will block until all events finished or m_ioService.stop() is called
-    m_ioService.run();
+    // m_ioCtx.run() will block until all events finished or m_ioCtx.stop() is called
+    m_ioCtx.run();
 
     // Alternatively, m_face.processEvents() can also be called.
     // processEvents will block until the requested data received or timeout occurs.
@@ -100,10 +100,10 @@
   }
 
 private:
-  // Explicitly create io_service object, which will be shared between Face and Scheduler
-  boost::asio::io_service m_ioService;
-  Face m_face{m_ioService};
-  Scheduler m_scheduler{m_ioService};
+  // Explicitly create io_context object, which will be shared between Face and Scheduler
+  boost::asio::io_context m_ioCtx;
+  Face m_face{m_ioCtx};
+  Scheduler m_scheduler{m_ioCtx};
 };
 
 } // namespace examples
diff --git a/ndn-cxx/detail/asio-fwd.hpp b/ndn-cxx/detail/asio-fwd.hpp
index a33b122..64b8ab3 100644
--- a/ndn-cxx/detail/asio-fwd.hpp
+++ b/ndn-cxx/detail/asio-fwd.hpp
@@ -22,13 +22,8 @@
 #ifndef NDN_CXX_DETAIL_ASIO_FWD_HPP
 #define NDN_CXX_DETAIL_ASIO_FWD_HPP
 
-#include <boost/version.hpp>
-
 namespace boost::asio {
-
 class io_context;
-using io_service = io_context;
-
 } // namespace boost::asio
 
 #endif // NDN_CXX_DETAIL_ASIO_FWD_HPP
diff --git a/ndn-cxx/detail/common.hpp b/ndn-cxx/detail/common.hpp
index 9071d8c..abc6bf0 100644
--- a/ndn-cxx/detail/common.hpp
+++ b/ndn-cxx/detail/common.hpp
@@ -68,10 +68,6 @@
 using std::make_shared;
 using std::make_unique;
 
-using std::static_pointer_cast;
-using std::dynamic_pointer_cast;
-using std::const_pointer_cast;
-
 using std::to_string;
 using namespace std::string_literals;
 using namespace std::string_view_literals;
diff --git a/ndn-cxx/detail/tag-host.hpp b/ndn-cxx/detail/tag-host.hpp
index 4bf59a3..05b3c41 100644
--- a/ndn-cxx/detail/tag-host.hpp
+++ b/ndn-cxx/detail/tag-host.hpp
@@ -73,7 +73,7 @@
   static_assert(std::is_convertible_v<T*, Tag*>, "T must inherit from ndn::Tag");
 
   if (auto it = m_tags.find(T::getTypeId()); it != m_tags.end()) {
-    return static_pointer_cast<T>(it->second);
+    return std::static_pointer_cast<T>(it->second);
   }
   return nullptr;
 }
diff --git a/ndn-cxx/encoding/encoder.cpp b/ndn-cxx/encoding/encoder.cpp
index 883d272..3c53352 100644
--- a/ndn-cxx/encoding/encoder.cpp
+++ b/ndn-cxx/encoding/encoder.cpp
@@ -34,7 +34,7 @@
 }
 
 Encoder::Encoder(const Block& block)
-  : m_buffer(const_pointer_cast<Buffer>(block.getBuffer()))
+  : m_buffer(std::const_pointer_cast<Buffer>(block.getBuffer()))
   , m_begin(m_buffer->begin() + (block.begin() - m_buffer->begin()))
   , m_end(m_buffer->begin()   + (block.end()   - m_buffer->begin()))
 {
diff --git a/ndn-cxx/face.cpp b/ndn-cxx/face.cpp
index f4d685c..bc590f0 100644
--- a/ndn-cxx/face.cpp
+++ b/ndn-cxx/face.cpp
@@ -20,31 +20,20 @@
  */
 
 #include "ndn-cxx/face.hpp"
+
 #include "ndn-cxx/encoding/tlv.hpp"
 #include "ndn-cxx/impl/face-impl.hpp"
 #include "ndn-cxx/net/face-uri.hpp"
+#include "ndn-cxx/transport/tcp-transport.hpp"
+#include "ndn-cxx/transport/unix-transport.hpp"
 #include "ndn-cxx/util/config-file.hpp"
 #include "ndn-cxx/util/scope.hpp"
 #include "ndn-cxx/util/time.hpp"
 
-// NDN_LOG_INIT(ndn.Face) is declared in face-impl.hpp
-
-// A callback scheduled through io.post and io.dispatch may be invoked after the face is destructed.
-// To prevent this situation, use these macros to capture Face::m_impl as weak_ptr and skip callback
-// execution if the face has been destructed.
-#define IO_CAPTURE_WEAK_IMPL(OP) \
-  { \
-    weak_ptr<Impl> implWeak(m_impl); \
-    m_ioService.OP([=] { \
-      auto impl = implWeak.lock(); \
-      if (impl != nullptr) {
-#define IO_CAPTURE_WEAK_IMPL_END \
-      } \
-    }); \
-  }
-
 namespace ndn {
 
+// NDN_LOG_INIT(ndn.Face) is declared in face-impl.hpp
+
 Face::OversizedPacketError::OversizedPacketError(char pktType, const Name& name, size_t wireSize)
   : Error((pktType == 'I' ? "Interest " : pktType == 'D' ? "Data " : "Nack ") +
           name.toUri() + " encodes into " + std::to_string(wireSize) + " octets, "
@@ -56,50 +45,50 @@
 }
 
 Face::Face(shared_ptr<Transport> transport)
-  : m_internalIoService(make_unique<boost::asio::io_service>())
-  , m_ioService(*m_internalIoService)
+  : m_internalIoCtx(make_unique<boost::asio::io_context>())
+  , m_ioCtx(*m_internalIoCtx)
   , m_internalKeyChain(make_unique<KeyChain>())
 {
   construct(std::move(transport), *m_internalKeyChain);
 }
 
-Face::Face(boost::asio::io_service& ioService)
-  : m_ioService(ioService)
+Face::Face(boost::asio::io_context& ioCtx)
+  : m_ioCtx(ioCtx)
   , m_internalKeyChain(make_unique<KeyChain>())
 {
   construct(nullptr, *m_internalKeyChain);
 }
 
 Face::Face(const std::string& host, const std::string& port)
-  : m_internalIoService(make_unique<boost::asio::io_service>())
-  , m_ioService(*m_internalIoService)
+  : m_internalIoCtx(make_unique<boost::asio::io_context>())
+  , m_ioCtx(*m_internalIoCtx)
   , m_internalKeyChain(make_unique<KeyChain>())
 {
   construct(make_shared<TcpTransport>(host, port), *m_internalKeyChain);
 }
 
 Face::Face(shared_ptr<Transport> transport, KeyChain& keyChain)
-  : m_internalIoService(make_unique<boost::asio::io_service>())
-  , m_ioService(*m_internalIoService)
+  : m_internalIoCtx(make_unique<boost::asio::io_context>())
+  , m_ioCtx(*m_internalIoCtx)
 {
   construct(std::move(transport), keyChain);
 }
 
-Face::Face(shared_ptr<Transport> transport, boost::asio::io_service& ioService)
-  : m_ioService(ioService)
+Face::Face(shared_ptr<Transport> transport, boost::asio::io_context& ioCtx)
+  : m_ioCtx(ioCtx)
   , m_internalKeyChain(make_unique<KeyChain>())
 {
   construct(std::move(transport), *m_internalKeyChain);
 }
 
-Face::Face(shared_ptr<Transport> transport, boost::asio::io_service& ioService, KeyChain& keyChain)
-  : m_ioService(ioService)
+Face::Face(shared_ptr<Transport> transport, boost::asio::io_context& ioCtx, KeyChain& keyChain)
+  : m_ioCtx(ioCtx)
 {
   construct(std::move(transport), keyChain);
 }
 
-shared_ptr<Transport>
-Face::makeDefaultTransport()
+static shared_ptr<Transport>
+makeDefaultTransport()
 {
   std::string transportUri;
 
@@ -117,11 +106,8 @@
     return UnixTransport::create("");
   }
 
-  std::string protocol;
   try {
-    FaceUri uri(transportUri);
-    protocol = uri.getScheme();
-
+    std::string protocol = FaceUri(transportUri).getScheme();
     if (protocol == "unix") {
       return UnixTransport::create(transportUri);
     }
@@ -152,9 +138,11 @@
   }
   m_transport = std::move(transport);
 
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->ensureConnected(false);
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->ensureConnected(false);
+    }
+  });
 }
 
 Face::~Face() = default;
@@ -166,13 +154,14 @@
                       const TimeoutCallback& afterTimeout)
 {
   auto id = m_impl->m_pendingInterestTable.allocateId();
-
   auto interest2 = make_shared<Interest>(interest);
   interest2->getNonce();
 
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->expressInterest(id, interest2, afterSatisfied, afterNacked, afterTimeout);
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [=, w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->expressInterest(id, interest2, afterSatisfied, afterNacked, afterTimeout);
+    }
+  });
 
   return PendingInterestHandle(m_impl, id);
 }
@@ -180,9 +169,11 @@
 void
 Face::removeAllPendingInterests()
 {
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->removeAllPendingInterests();
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->removeAllPendingInterests();
+    }
+  });
 }
 
 size_t
@@ -192,19 +183,23 @@
 }
 
 void
-Face::put(Data data)
+Face::put(const Data& data)
 {
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->putData(data);
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [data, w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->putData(data);
+    }
+  });
 }
 
 void
-Face::put(lp::Nack nack)
+Face::put(const lp::Nack& nack)
 {
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->putNack(nack);
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [nack, w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->putNack(nack);
+    }
+  });
 }
 
 RegisteredPrefixHandle
@@ -234,9 +229,11 @@
 {
   auto id = m_impl->m_interestFilterTable.allocateId();
 
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->setInterestFilter(id, filter, onInterest);
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [=, w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->setInterestFilter(id, filter, onInterest);
+    }
+  });
 
   return InterestFilterHandle(m_impl, id);
 }
@@ -256,44 +253,47 @@
 }
 
 void
-Face::doProcessEvents(time::milliseconds timeout, bool keepThread)
+Face::doProcessEvents(time::milliseconds timeout, bool keepRunning)
 {
-  if (m_ioService.stopped()) {
-    m_ioService.reset(); // ensure that run()/poll() will do some work
+  if (m_ioCtx.stopped()) {
+    m_ioCtx.restart(); // ensure that run()/poll() will do some work
   }
 
   auto onThrow = make_scope_fail([this] { m_impl->shutdown(); });
 
   if (timeout < 0_ms) {
     // do not block if timeout is negative, but process pending events
-    m_ioService.poll();
+    m_ioCtx.poll();
     return;
   }
 
   if (timeout > 0_ms) {
+    // TODO: consider using m_ioCtx.run_for(timeout) in this case
     m_impl->m_processEventsTimeoutEvent = m_impl->m_scheduler.schedule(timeout,
-      [&io = m_ioService, &work = m_impl->m_ioServiceWork] {
+      [&io = m_ioCtx, &work = m_impl->m_workGuard] {
         io.stop();
         work.reset();
       });
   }
 
-  if (keepThread) {
-    // work will ensure that m_ioService is running until work object exists
-    m_impl->m_ioServiceWork = make_unique<boost::asio::io_service::work>(m_ioService);
+  if (keepRunning) {
+    // m_workGuard ensures that m_ioCtx keeps running even when it runs out of work (events)
+    m_impl->m_workGuard = make_unique<Impl::IoContextWorkGuard>(m_ioCtx.get_executor());
   }
 
-  m_ioService.run();
+  m_ioCtx.run();
 }
 
 void
 Face::shutdown()
 {
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->shutdown();
-    if (m_transport->getState() != Transport::State::CLOSED)
-      m_transport->close();
-  } IO_CAPTURE_WEAK_IMPL_END
+  boost::asio::post(m_ioCtx, [this, w = m_impl->weak_from_this()] {
+    if (auto impl = w.lock(); impl != nullptr) {
+      impl->shutdown();
+      if (m_transport->getState() != Transport::State::CLOSED)
+        m_transport->close();
+    }
+  });
 }
 
 /**
@@ -344,8 +344,7 @@
 
 PendingInterestHandle::PendingInterestHandle(weak_ptr<Face::Impl> weakImpl, detail::RecordId id)
   : CancelHandle([w = std::move(weakImpl), id] {
-      auto impl = w.lock();
-      if (impl != nullptr) {
+      if (auto impl = w.lock(); impl != nullptr) {
         impl->asyncRemovePendingInterest(id);
       }
     })
@@ -381,8 +380,7 @@
                                    const UnregisterPrefixSuccessCallback& onSuccess,
                                    const UnregisterPrefixFailureCallback& onFailure)
 {
-  auto impl = weakImpl.lock();
-  if (impl != nullptr) {
+  if (auto impl = weakImpl.lock(); impl != nullptr) {
     impl->asyncUnregisterPrefix(id, onSuccess, onFailure);
   }
   else if (onFailure) {
@@ -392,8 +390,7 @@
 
 InterestFilterHandle::InterestFilterHandle(weak_ptr<Face::Impl> weakImpl, detail::RecordId id)
   : CancelHandle([w = std::move(weakImpl), id] {
-      auto impl = w.lock();
-      if (impl != nullptr) {
+      if (auto impl = w.lock(); impl != nullptr) {
         impl->asyncUnsetInterestFilter(id);
       }
     })
diff --git a/ndn-cxx/face.hpp b/ndn-cxx/face.hpp
index b915398..a5a1b46 100644
--- a/ndn-cxx/face.hpp
+++ b/ndn-cxx/face.hpp
@@ -118,7 +118,7 @@
 
 public: // constructors
   /**
-   * @brief Create Face using given transport (or default transport if omitted)
+   * @brief Create Face using the given Transport (or default transport if omitted).
    * @param transport the transport for lower layer communication. If nullptr,
    *                  a default transport will be used. The default transport is
    *                  determined from a FaceUri in NDN_CLIENT_TRANSPORT environ,
@@ -132,38 +132,36 @@
   Face(shared_ptr<Transport> transport = nullptr);
 
   /**
-   * @brief Create Face using default transport and given io_service
+   * @brief Create Face using default transport and given io_context.
    *
    * Usage examples:
    *
    * @code
    * Face face1;
-   * Face face2(face1.getIoService());
+   * Face face2(face1.getIoContext());
    *
    * // Now the following ensures that events on both faces are processed
    * face1.processEvents();
-   * // or face1.getIoService().run();
+   * // or face1.getIoContext().run();
    * @endcode
    *
    * or
    *
    * @code
-   * boost::asio::io_service ioService;
-   * Face face1(ioService);
-   * Face face2(ioService);
-   *
-   * ioService.run();
+   * boost::asio::io_context ioCtx;
+   * Face face1(ioCtx);
+   * Face face2(ioCtx);
+   * ioCtx.run();
    * @endcode
    *
-   * @param ioService A reference to boost::io_service object that should control all
-   *                  IO operations.
+   * @param ioCtx A reference to the io_context object that should control all I/O operations.
    * @throw ConfigFile::Error the configuration file cannot be parsed or specifies an unsupported protocol
    */
   explicit
-  Face(boost::asio::io_service& ioService);
+  Face(boost::asio::io_context& ioCtx);
 
   /**
-   * @brief Create Face using TcpTransport
+   * @brief Create Face using TcpTransport.
    *
    * @param host IP address or hostname of the NDN forwarder
    * @param port port number or service name of the NDN forwarder (**default**: "6363")
@@ -172,7 +170,7 @@
   Face(const std::string& host, const std::string& port = "6363");
 
   /**
-   * @brief Create Face using given transport and KeyChain
+   * @brief Create Face using the given Transport and KeyChain.
    * @param transport the transport for lower layer communication. If nullptr,
    *                  a default transport will be used.
    * @param keyChain the KeyChain to sign commands
@@ -186,35 +184,35 @@
   Face(shared_ptr<Transport> transport, KeyChain& keyChain);
 
   /**
-   * @brief Create Face using given transport and io_service
+   * @brief Create Face using the given Transport and io_context.
    * @param transport the transport for lower layer communication. If nullptr,
    *                  a default transport will be used.
-   * @param ioService the io_service that controls all IO operations
+   * @param ioCtx the io_context that controls all I/O operations
    *
-   * @sa Face(boost::asio::io_service&)
+   * @sa Face(boost::asio::io_context&)
    * @sa Face(shared_ptr<Transport>)
    *
    * @throw ConfigFile::Error @p transport is nullptr, and the configuration file cannot be
    *                          parsed or specifies an unsupported protocol
    * @note shared_ptr is passed by value because ownership is shared with this class
    */
-  Face(shared_ptr<Transport> transport, boost::asio::io_service& ioService);
+  Face(shared_ptr<Transport> transport, boost::asio::io_context& ioCtx);
 
   /**
-   * @brief Create a new Face using given Transport and io_service
+   * @brief Create Face using the given Transport, io_context, and KeyChain.
    * @param transport the transport for lower layer communication. If nullptr,
    *                  a default transport will be used.
-   * @param ioService the io_service that controls all IO operations
+   * @param ioCtx the io_context that controls all I/O operations
    * @param keyChain the KeyChain to sign commands
    *
-   * @sa Face(boost::asio::io_service&)
+   * @sa Face(boost::asio::io_context&)
    * @sa Face(shared_ptr<Transport>, KeyChain&)
    *
    * @throw ConfigFile::Error @p transport is nullptr, and the configuration file cannot be
    *                          parsed or specifies an unsupported protocol
    * @note shared_ptr is passed by value because ownership is shared with this class
    */
-  Face(shared_ptr<Transport> transport, boost::asio::io_service& ioService, KeyChain& keyChain);
+  Face(shared_ptr<Transport> transport, boost::asio::io_context& ioCtx, KeyChain& keyChain);
 
   virtual
   ~Face();
@@ -228,8 +226,8 @@
    * @param afterNacked function to be invoked if Network NACK is returned
    * @param afterTimeout function to be invoked if neither Data nor Network NACK
    *                     is returned within InterestLifetime
-   * @throw OversizedPacketError encoded Interest size exceeds MAX_NDN_PACKET_SIZE
    * @return A handle for canceling the pending Interest.
+   * @throw OversizedPacketError Encoded Interest size exceeds #MAX_NDN_PACKET_SIZE.
    */
   PendingInterestHandle
   expressInterest(const Interest& interest,
@@ -343,61 +341,62 @@
 
   /**
    * @brief Publish a Data packet.
-   * @param data the Data; a copy will be made, so that the caller is not required to
-   *             maintain the argument unchanged
+   * @param data The Data packet; a copy will be made, so that the caller is not required to
+   *             maintain the argument unchanged.
    *
    * This method can be called to satisfy incoming Interests, or to add Data packet into the cache
    * of the local NDN forwarder if forwarder is configured to accept unsolicited Data.
    *
-   * @throw OversizedPacketError encoded Data size exceeds MAX_NDN_PACKET_SIZE
+   * @throw OversizedPacketError Encoded Data size exceeds #MAX_NDN_PACKET_SIZE.
    */
   void
-  put(Data data);
+  put(const Data& data);
 
   /**
    * @brief Send a %Network Nack.
-   * @param nack the Nack; a copy will be made, so that the caller is not required to
-   *             maintain the argument unchanged
-   * @throw OversizedPacketError encoded Nack size exceeds MAX_NDN_PACKET_SIZE
+   * @param nack The Nack packet; a copy will be made, so that the caller is not required to
+   *             maintain the argument unchanged.
+   * @throw OversizedPacketError Encoded Nack size exceeds #MAX_NDN_PACKET_SIZE.
    */
   void
-  put(lp::Nack nack);
+  put(const lp::Nack& nack);
 
-public: // IO routine
+public: // event loop routines
   /**
-   * @brief Process any data to receive or call timeout callbacks.
+   * @brief Run the event loop to process any pending work and execute completion handlers.
    *
-   * This call will block forever (with the default timeout of 0) to process I/O on the face.
+   * This call will block forever (with the default @p timeout of 0) to process I/O on the face.
    * To exit cleanly on a producer, clear any Interest filters and wait for processEvents() to
    * return. To exit after an error, one can call shutdown().
    * In consumer applications, processEvents() will return when all expressed Interests have been
    * satisfied, Nacked, or timed out. To terminate earlier, a consumer application should cancel
    * all previously expressed and still-pending Interests.
    *
-   * If a positive timeout is specified, then processEvents() will exit after this timeout, provided
-   * it is not stopped earlier with shutdown() or when all active events finish. processEvents()
-   * can be called repeatedly, if desired.
+   * If @p timeout is a positive value, then processEvents() will return after the specified
+   * duration has elapsed, unless the event loop is stopped earlier with shutdown() or runs
+   * out of work to do.
    *
-   * If a negative timeout is specified, then processEvents will not block and will process only
-   * pending events.
+   * If a negative @p timeout is specified, then processEvents() will not block and will process
+   * only handlers that are ready to run.
    *
-   * @param timeout     maximum time to block the thread
-   * @param keepThread  Keep thread in a blocked state (in event processing), even when
+   * processEvents() can be called repeatedly, if desired.
+   *
+   * @param timeout     Maximum amount of time to block the event loop (see above).
+   * @param keepRunning Keep thread in a blocked state (in event processing), even when
    *                    there are no outstanding events (e.g., no Interest/Data is expected).
-   *                    If timeout is zero and this parameter is true, the only way to stop
-   *                    processEvents() is to call shutdown().
+   *                    Ignored if @p timeout is negative. If @p timeout is 0 and @p keepRunning
+   *                    is true, the only way to stop processEvents() is to call shutdown().
    *
    * @note This may throw an exception for reading data or in the callback for processing
    * the data.  If you call this from an main event loop, you may want to catch and
    * log/disregard all exceptions.
    *
-   * @throw OversizedPacketError encoded packet size exceeds MAX_NDN_PACKET_SIZE
+   * @throw OversizedPacketError Encoded packet size exceeds #MAX_NDN_PACKET_SIZE.
    */
   void
-  processEvents(time::milliseconds timeout = time::milliseconds::zero(),
-                bool keepThread = false)
+  processEvents(time::milliseconds timeout = 0_ms, bool keepRunning = false)
   {
-    this->doProcessEvents(timeout, keepThread);
+    this->doProcessEvents(timeout, keepRunning);
   }
 
   /**
@@ -405,8 +404,8 @@
    *
    * This method cancels all pending operations and closes the connection to the NDN forwarder.
    *
-   * Note that this method does not stop the io_service if it is shared between multiple Faces or
-   * with other IO objects (e.g., Scheduler).
+   * Note that this method does not stop the io_context if it is shared between multiple Faces or
+   * with other I/O objects (e.g., Scheduler).
    *
    * @warning Calling this method may cause outgoing packets to be lost. Producers that shut down
    *          immediately after sending a Data packet should instead clear all Interest filters to
@@ -417,36 +416,40 @@
   shutdown();
 
   /**
-   * @brief Returns a reference to the io_service used by this face.
+   * @brief Returns a reference to the io_context used by this face.
    */
-  boost::asio::io_service&
-  getIoService()
+  boost::asio::io_context&
+  getIoContext() const noexcept
   {
-    return m_ioService;
+    return m_ioCtx;
+  }
+
+  /**
+   * @deprecated Use getIoContext()
+   */
+  [[deprecated("use getIoContext")]]
+  boost::asio::io_context&
+  getIoService() const noexcept
+  {
+    return m_ioCtx;
   }
 
 NDN_CXX_PUBLIC_WITH_TESTS_ELSE_PROTECTED:
   /**
    * @brief Returns the underlying transport.
    */
-  shared_ptr<Transport>
+  Transport&
   getTransport() const
   {
-    return m_transport;
+    return *m_transport;
   }
 
 protected:
   virtual void
-  doProcessEvents(time::milliseconds timeout, bool keepThread);
+  doProcessEvents(time::milliseconds timeout, bool keepRunning);
 
 private:
   /**
-   * @throw ConfigFile::Error on parse error and unsupported protocols
-   */
-  shared_ptr<Transport>
-  makeDefaultTransport();
-
-  /**
    * @throw Face::Error on unsupported protocol
    */
   void
@@ -456,10 +459,10 @@
   onReceiveElement(const Block& blockFromDaemon);
 
 private:
-  /// the io_service owned by this Face, may be null
-  unique_ptr<boost::asio::io_service> m_internalIoService;
-  /// the io_service used by this Face
-  boost::asio::io_service& m_ioService;
+  /// The io_context owned by this Face, may be null if using an externally provided io_context.
+  unique_ptr<boost::asio::io_context> m_internalIoCtx;
+  /// The io_context used by this Face.
+  boost::asio::io_context& m_ioCtx;
 
   shared_ptr<Transport> m_transport;
 
diff --git a/ndn-cxx/impl/face-impl.hpp b/ndn-cxx/impl/face-impl.hpp
index 54b1ffe..df1fabd 100644
--- a/ndn-cxx/impl/face-impl.hpp
+++ b/ndn-cxx/impl/face-impl.hpp
@@ -31,12 +31,16 @@
 #include "ndn-cxx/lp/tags.hpp"
 #include "ndn-cxx/mgmt/nfd/command-options.hpp"
 #include "ndn-cxx/mgmt/nfd/controller.hpp"
-#include "ndn-cxx/transport/tcp-transport.hpp"
-#include "ndn-cxx/transport/unix-transport.hpp"
+#include "ndn-cxx/transport/transport.hpp"
 #include "ndn-cxx/util/logger.hpp"
 #include "ndn-cxx/util/scheduler.hpp"
 
-NDN_LOG_INIT(ndn.Face);
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
+
+namespace ndn {
+
+//
 // INFO level: prefix registration, etc.
 //
 // DEBUG level: packet logging.
@@ -49,17 +53,18 @@
 // DEBUG level.
 //
 // TRACE level: more detailed unstructured messages.
+//
+NDN_LOG_INIT(ndn.Face);
 
-namespace ndn {
-
-/** @brief Implementation detail of Face.
+/**
+ * @brief Implementation detail of Face.
  */
 class Face::Impl : public std::enable_shared_from_this<Face::Impl>
 {
 public:
   Impl(Face& face, KeyChain& keyChain)
     : m_face(face)
-    , m_scheduler(m_face.getIoService())
+    , m_scheduler(m_face.getIoContext())
     , m_nfdController(m_face, keyChain)
   {
     auto onEmptyPitOrNoRegisteredPrefixes = [this] {
@@ -67,10 +72,10 @@
       // (+async_read) from within onInterest/onData callback.  After onInterest/onData
       // finishes, there is another +async_read with the same memory block.  A few of such
       // async_read duplications can cause various effects and result in segfault.
-      m_face.getIoService().post([this] {
+      boost::asio::post(m_face.getIoContext(), [this] {
         if (m_pendingInterestTable.empty() && m_registeredPrefixTable.empty()) {
           m_face.m_transport->pause();
-          if (!m_ioServiceWork) {
+          if (!m_workGuard) {
             m_processEventsTimeoutEvent.cancel();
           }
         }
@@ -108,9 +113,8 @@
   void
   asyncRemovePendingInterest(detail::RecordId id)
   {
-    m_face.getIoService().post([id, w = weak_from_this()] {
-      auto impl = w.lock();
-      if (impl != nullptr) {
+    boost::asio::post(m_face.getIoContext(), [id, w = weak_from_this()] {
+      if (auto impl = w.lock(); impl != nullptr) {
         impl->m_pendingInterestTable.erase(id);
       }
     });
@@ -194,9 +198,8 @@
   void
   asyncUnsetInterestFilter(detail::RecordId id)
   {
-    m_face.getIoService().post([id, w = weak_from_this()] {
-      auto impl = w.lock();
-      if (impl != nullptr) {
+    boost::asio::post(m_face.getIoContext(), [id, w = weak_from_this()] {
+      if (auto impl = w.lock(); impl != nullptr) {
         impl->unsetInterestFilter(id);
       }
     });
@@ -293,9 +296,8 @@
                         const UnregisterPrefixSuccessCallback& onSuccess,
                         const UnregisterPrefixFailureCallback& onFailure)
   {
-    m_face.getIoService().post([=, w = weak_from_this()] {
-      auto impl = w.lock();
-      if (impl != nullptr) {
+    boost::asio::post(m_face.getIoContext(), [=, w = weak_from_this()] {
+      if (auto impl = w.lock(); impl != nullptr) {
         impl->unregisterPrefix(id, onSuccess, onFailure);
       }
     });
@@ -306,7 +308,7 @@
   ensureConnected(bool wantResume)
   {
     if (m_face.m_transport->getState() == Transport::State::CLOSED) {
-      m_face.m_transport->connect(m_face.getIoService(),
+      m_face.m_transport->connect(m_face.getIoContext(),
                                   [this] (const Block& wire) { m_face.onReceiveElement(wire); });
     }
 
@@ -318,7 +320,7 @@
   void
   shutdown()
   {
-    m_ioServiceWork.reset();
+    m_workGuard.reset();
     m_pendingInterestTable.clear();
     m_registeredPrefixTable.clear();
   }
@@ -417,7 +419,8 @@
   detail::RecordContainer<InterestFilterRecord> m_interestFilterTable;
   detail::RecordContainer<RegisteredPrefix> m_registeredPrefixTable;
 
-  unique_ptr<boost::asio::io_service::work> m_ioServiceWork; // if thread needs to be preserved
+  using IoContextWorkGuard = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;
+  unique_ptr<IoContextWorkGuard> m_workGuard;
 
   friend Face;
 };
diff --git a/ndn-cxx/ims/in-memory-storage-fifo.cpp b/ndn-cxx/ims/in-memory-storage-fifo.cpp
index 976170b..c3c2e79 100644
--- a/ndn-cxx/ims/in-memory-storage-fifo.cpp
+++ b/ndn-cxx/ims/in-memory-storage-fifo.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -28,8 +28,8 @@
 {
 }
 
-InMemoryStorageFifo::InMemoryStorageFifo(boost::asio::io_service& ioService, size_t limit)
-  : InMemoryStorage(ioService, limit)
+InMemoryStorageFifo::InMemoryStorageFifo(boost::asio::io_context& ioCtx, size_t limit)
+  : InMemoryStorage(ioCtx, limit)
 {
 }
 
@@ -44,7 +44,7 @@
 InMemoryStorageFifo::evictItem()
 {
   if (!m_cleanupIndex.get<byArrival>().empty()) {
-    CleanupIndex::index<byArrival>::type::iterator it = m_cleanupIndex.get<byArrival>().begin();
+    auto it = m_cleanupIndex.get<byArrival>().begin();
     eraseImpl((*it)->getFullName());
     m_cleanupIndex.get<byArrival>().erase(it);
     return true;
@@ -56,7 +56,7 @@
 void
 InMemoryStorageFifo::beforeErase(InMemoryStorageEntry* entry)
 {
-  CleanupIndex::index<byEntity>::type::iterator it = m_cleanupIndex.get<byEntity>().find(entry);
+  auto it = m_cleanupIndex.get<byEntity>().find(entry);
   if (it != m_cleanupIndex.get<byEntity>().end())
     m_cleanupIndex.get<byEntity>().erase(it);
 }
diff --git a/ndn-cxx/ims/in-memory-storage-fifo.hpp b/ndn-cxx/ims/in-memory-storage-fifo.hpp
index cc59acf..96193d5 100644
--- a/ndn-cxx/ims/in-memory-storage-fifo.hpp
+++ b/ndn-cxx/ims/in-memory-storage-fifo.hpp
@@ -30,7 +30,8 @@
 
 namespace ndn {
 
-/** @brief Provides in-memory storage employing First-In-First-Out (FIFO) replacement policy.
+/**
+ * @brief Provides in-memory storage employing First-In-First-Out (FIFO) replacement policy.
  */
 class InMemoryStorageFifo : public InMemoryStorage
 {
@@ -39,7 +40,7 @@
   InMemoryStorageFifo(size_t limit = 16);
 
   explicit
-  InMemoryStorageFifo(boost::asio::io_service& ioService, size_t limit = 16);
+  InMemoryStorageFifo(boost::asio::io_context& ioCtx, size_t limit = 16);
 
 NDN_CXX_PUBLIC_WITH_TESTS_ELSE_PROTECTED:
   /** @brief Removes one Data packet from in-memory storage based on FIFO
diff --git a/ndn-cxx/ims/in-memory-storage-lfu.cpp b/ndn-cxx/ims/in-memory-storage-lfu.cpp
index 2e5e333..cb72530 100644
--- a/ndn-cxx/ims/in-memory-storage-lfu.cpp
+++ b/ndn-cxx/ims/in-memory-storage-lfu.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -28,8 +28,8 @@
 {
 }
 
-InMemoryStorageLfu::InMemoryStorageLfu(boost::asio::io_service& ioService, size_t limit)
-  : InMemoryStorage(ioService, limit)
+InMemoryStorageLfu::InMemoryStorageLfu(boost::asio::io_context& ioCtx, size_t limit)
+  : InMemoryStorage(ioCtx, limit)
 {
 }
 
@@ -47,7 +47,7 @@
 InMemoryStorageLfu::evictItem()
 {
   if (!m_cleanupIndex.get<byFrequency>().empty()) {
-    CleanupIndex::index<byFrequency>::type::iterator it = m_cleanupIndex.get<byFrequency>().begin();
+    auto it = m_cleanupIndex.get<byFrequency>().begin();
     eraseImpl(((*it).entry)->getFullName());
     m_cleanupIndex.get<byFrequency>().erase(it);
     return true;
@@ -59,7 +59,7 @@
 void
 InMemoryStorageLfu::beforeErase(InMemoryStorageEntry* entry)
 {
-  CleanupIndex::index<byEntity>::type::iterator it = m_cleanupIndex.get<byEntity>().find(entry);
+  auto it = m_cleanupIndex.get<byEntity>().find(entry);
   if (it != m_cleanupIndex.get<byEntity>().end())
     m_cleanupIndex.get<byEntity>().erase(it);
 }
@@ -67,7 +67,7 @@
 void
 InMemoryStorageLfu::afterAccess(InMemoryStorageEntry* entry)
 {
-  CleanupIndex::index<byEntity>::type::iterator it = m_cleanupIndex.get<byEntity>().find(entry);
+  auto it = m_cleanupIndex.get<byEntity>().find(entry);
   m_cleanupIndex.get<byEntity>().modify(it, &incrementFrequency);
 }
 
diff --git a/ndn-cxx/ims/in-memory-storage-lfu.hpp b/ndn-cxx/ims/in-memory-storage-lfu.hpp
index cc8e7dc..ec72c6e 100644
--- a/ndn-cxx/ims/in-memory-storage-lfu.hpp
+++ b/ndn-cxx/ims/in-memory-storage-lfu.hpp
@@ -32,9 +32,9 @@
 
 namespace ndn {
 
-/** @brief Provides an in-memory storage with Least Frequently Used (LFU) replacement policy.
- *  @note The frequency right now is usage count.
- *  @sa https://en.wikipedia.org/w/index.php?title=Least_frequently_used&oldid=604542656
+/**
+ * @brief Provides an in-memory storage with Least Frequently Used (LFU) replacement policy.
+ * @note The frequency right now is usage count.
  */
 class InMemoryStorageLfu : public InMemoryStorage
 {
@@ -43,7 +43,7 @@
   InMemoryStorageLfu(size_t limit = 16);
 
   explicit
-  InMemoryStorageLfu(boost::asio::io_service& ioService, size_t limit = 16);
+  InMemoryStorageLfu(boost::asio::io_context& ioCtx, size_t limit = 16);
 
 NDN_CXX_PUBLIC_WITH_TESTS_ELSE_PROTECTED:
   /** @brief Removes one Data packet from in-memory storage based on LFU, i.e. evict the least
diff --git a/ndn-cxx/ims/in-memory-storage-lru.cpp b/ndn-cxx/ims/in-memory-storage-lru.cpp
index f80f854..40b4d92 100644
--- a/ndn-cxx/ims/in-memory-storage-lru.cpp
+++ b/ndn-cxx/ims/in-memory-storage-lru.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -28,9 +28,8 @@
 {
 }
 
-InMemoryStorageLru::InMemoryStorageLru(boost::asio::io_service& ioService,
-                                       size_t limit)
-  : InMemoryStorage(ioService, limit)
+InMemoryStorageLru::InMemoryStorageLru(boost::asio::io_context& ioCtx, size_t limit)
+  : InMemoryStorage(ioCtx, limit)
 {
 }
 
@@ -46,7 +45,7 @@
 InMemoryStorageLru::evictItem()
 {
   if (!m_cleanupIndex.get<byUsedTime>().empty()) {
-    CleanupIndex::index<byUsedTime>::type::iterator it = m_cleanupIndex.get<byUsedTime>().begin();
+    auto it = m_cleanupIndex.get<byUsedTime>().begin();
     eraseImpl((*it)->getFullName());
     m_cleanupIndex.get<byUsedTime>().erase(it);
     return true;
@@ -58,7 +57,7 @@
 void
 InMemoryStorageLru::beforeErase(InMemoryStorageEntry* entry)
 {
-  CleanupIndex::index<byEntity>::type::iterator it = m_cleanupIndex.get<byEntity>().find(entry);
+  auto it = m_cleanupIndex.get<byEntity>().find(entry);
   if (it != m_cleanupIndex.get<byEntity>().end())
     m_cleanupIndex.get<byEntity>().erase(it);
 }
diff --git a/ndn-cxx/ims/in-memory-storage-lru.hpp b/ndn-cxx/ims/in-memory-storage-lru.hpp
index 9f0decf..7dd5f29 100644
--- a/ndn-cxx/ims/in-memory-storage-lru.hpp
+++ b/ndn-cxx/ims/in-memory-storage-lru.hpp
@@ -32,7 +32,8 @@
 
 namespace ndn {
 
-/** @brief Provides in-memory storage employing Least Recently Used (LRU) replacement policy.
+/**
+ * @brief Provides in-memory storage employing Least Recently Used (LRU) replacement policy.
  */
 class InMemoryStorageLru : public InMemoryStorage
 {
@@ -41,7 +42,7 @@
   InMemoryStorageLru(size_t limit = 16);
 
   explicit
-  InMemoryStorageLru(boost::asio::io_service& ioService, size_t limit = 16);
+  InMemoryStorageLru(boost::asio::io_context& ioCtx, size_t limit = 16);
 
 NDN_CXX_PUBLIC_WITH_TESTS_ELSE_PROTECTED:
   /** @brief Removes one Data packet from in-memory storage based on LRU, i.e. evict the least
diff --git a/ndn-cxx/ims/in-memory-storage-persistent.cpp b/ndn-cxx/ims/in-memory-storage-persistent.cpp
deleted file mode 100644
index bd3c195..0000000
--- a/ndn-cxx/ims/in-memory-storage-persistent.cpp
+++ /dev/null
@@ -1,42 +0,0 @@
-/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/*
- * Copyright (c) 2013-2018 Regents of the University of California.
- *
- * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
- *
- * ndn-cxx library is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
- *
- * You should have received copies of the GNU General Public License and GNU Lesser
- * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
- * <http://www.gnu.org/licenses/>.
- *
- * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
- */
-
-#include "ndn-cxx/ims/in-memory-storage-persistent.hpp"
-
-namespace ndn {
-
-InMemoryStoragePersistent::InMemoryStoragePersistent()
-  : InMemoryStorage()
-{
-}
-
-InMemoryStoragePersistent::InMemoryStoragePersistent(boost::asio::io_service& ioService)
-  : InMemoryStorage(ioService)
-{
-}
-
-bool
-InMemoryStoragePersistent::evictItem()
-{
-  return false;
-}
-
-} // namespace ndn
diff --git a/ndn-cxx/ims/in-memory-storage-persistent.hpp b/ndn-cxx/ims/in-memory-storage-persistent.hpp
index 6710434..f6cdca3 100644
--- a/ndn-cxx/ims/in-memory-storage-persistent.hpp
+++ b/ndn-cxx/ims/in-memory-storage-persistent.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2021 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -26,26 +26,32 @@
 
 namespace ndn {
 
-/** @brief Provides application cache with persistent storage, of which no replacement policy will
- *  be employed. Entries will only be deleted by explicit application control.
+/**
+ * @brief Provides application cache with persistent storage, of which no replacement policy will
+ *        be employed. Entries will only be deleted by explicit application control.
  */
 class InMemoryStoragePersistent : public InMemoryStorage
 {
 public:
-  InMemoryStoragePersistent();
+  InMemoryStoragePersistent() = default;
 
   explicit
-  InMemoryStoragePersistent(boost::asio::io_service& ioService);
+  InMemoryStoragePersistent(boost::asio::io_context& ioCtx)
+    : InMemoryStorage(ioCtx)
+  {
+  }
 
 NDN_CXX_PUBLIC_WITH_TESTS_ELSE_PROTECTED:
-  /** @brief Do nothing.
+  /**
+   * @brief Do nothing.
    *
-   *  This storage is persistent, and does not support eviction.
-   *
-   *  @return false
+   * This storage is persistent, and thus does not support eviction.
    */
   bool
-  evictItem() override;
+  evictItem() override
+  {
+    return false;
+  }
 };
 
 } // namespace ndn
diff --git a/ndn-cxx/ims/in-memory-storage.cpp b/ndn-cxx/ims/in-memory-storage.cpp
index 36ccb08..c4ea1cf 100644
--- a/ndn-cxx/ims/in-memory-storage.cpp
+++ b/ndn-cxx/ims/in-memory-storage.cpp
@@ -47,11 +47,11 @@
   init();
 }
 
-InMemoryStorage::InMemoryStorage(boost::asio::io_service& ioService, size_t limit)
+InMemoryStorage::InMemoryStorage(boost::asio::io_context& ioCtx, size_t limit)
   : m_limit(limit)
   , m_nPackets(0)
 {
-  m_scheduler = make_unique<Scheduler>(ioService);
+  m_scheduler = make_unique<Scheduler>(ioCtx);
   init();
 }
 
diff --git a/ndn-cxx/ims/in-memory-storage.hpp b/ndn-cxx/ims/in-memory-storage.hpp
index 3096542..de4acde 100644
--- a/ndn-cxx/ims/in-memory-storage.hpp
+++ b/ndn-cxx/ims/in-memory-storage.hpp
@@ -117,7 +117,7 @@
    *  The InMemoryStorage created through this method will handle MustBeFresh in interest processing
    */
   explicit
-  InMemoryStorage(boost::asio::io_service& ioService,
+  InMemoryStorage(boost::asio::io_context& ioCtx,
                   size_t limit = std::numeric_limits<size_t>::max());
 
   /** @note Please make sure to implement it to free m_freeEntries and evict
diff --git a/ndn-cxx/mgmt/dispatcher.cpp b/ndn-cxx/mgmt/dispatcher.cpp
index a192642..b42fa36 100644
--- a/ndn-cxx/mgmt/dispatcher.cpp
+++ b/ndn-cxx/mgmt/dispatcher.cpp
@@ -47,7 +47,7 @@
   : m_face(face)
   , m_keyChain(keyChain)
   , m_signingInfo(signingInfo)
-  , m_storage(m_face.getIoService(), imsCapacity)
+  , m_storage(m_face.getIoContext(), imsCapacity)
 {
 }
 
diff --git a/ndn-cxx/net/dns.cpp b/ndn-cxx/net/dns.cpp
index 70d0d8a..36abaf0 100644
--- a/ndn-cxx/net/dns.cpp
+++ b/ndn-cxx/net/dns.cpp
@@ -22,7 +22,6 @@
 #include "ndn-cxx/net/dns.hpp"
 #include "ndn-cxx/util/scheduler.hpp"
 
-#include <boost/asio/io_service.hpp>
 #include <boost/asio/ip/udp.hpp>
 #include <boost/asio/post.hpp>
 
@@ -31,22 +30,17 @@
 class Resolver : noncopyable
 {
 public:
-  using protocol = boost::asio::ip::udp;
-  using iterator = protocol::resolver::iterator;
-  using query = protocol::resolver::query;
-
-public:
-  Resolver(boost::asio::io_service& ioService,
+  Resolver(boost::asio::io_context& ioCtx,
            const AddressSelector& addressSelector)
-    : m_resolver(ioService)
+    : m_resolver(ioCtx)
     , m_addressSelector(addressSelector)
-    , m_scheduler(ioService)
+    , m_scheduler(ioCtx)
   {
     BOOST_ASSERT(m_addressSelector != nullptr);
   }
 
   void
-  asyncResolve(const query& q,
+  asyncResolve(const std::string& host,
                const SuccessCallback& onSuccess,
                const ErrorCallback& onError,
                time::nanoseconds timeout,
@@ -55,23 +49,18 @@
     m_onSuccess = onSuccess;
     m_onError = onError;
 
-    m_resolver.async_resolve(q, [=] (auto&&... args) {
+    m_resolver.async_resolve(host, "", [=] (auto&&... args) {
       onResolveResult(std::forward<decltype(args)>(args)..., self);
     });
 
     m_resolveTimeout = m_scheduler.schedule(timeout, [=] { onResolveTimeout(self); });
   }
 
-  iterator
-  syncResolve(const query& q)
-  {
-    return selectAddress(m_resolver.resolve(q));
-  }
-
 private:
   void
   onResolveResult(const boost::system::error_code& error,
-                  iterator it, const shared_ptr<Resolver>& self)
+                  const boost::asio::ip::udp::resolver::results_type& results,
+                  const shared_ptr<Resolver>& self)
   {
     m_resolveTimeout.cancel();
 
@@ -79,21 +68,22 @@
     boost::asio::post(m_resolver.get_executor(), [self] {});
 
     if (error) {
-      if (error == boost::asio::error::operation_aborted)
-        return;
-
-      if (m_onError)
-        m_onError("Hostname cannot be resolved: " + error.message());
-
+      if (m_onError && error != boost::asio::error::operation_aborted) {
+        m_onError("Hostname could not be resolved: " + error.message());
+      }
       return;
     }
 
-    it = selectAddress(it);
-
-    if (it != iterator() && m_onSuccess) {
-      m_onSuccess(it->endpoint().address());
+    for (const auto& entry : results) {
+      if (m_addressSelector(entry.endpoint().address())) {
+        if (m_onSuccess) {
+          m_onSuccess(entry.endpoint().address());
+        }
+        return;
+      }
     }
-    else if (m_onError) {
+
+    if (m_onError) {
       m_onError("No endpoints match the specified address selector");
     }
   }
@@ -106,21 +96,13 @@
     // ensure the Resolver isn't destructed while callbacks are still pending, see #2653
     boost::asio::post(m_resolver.get_executor(), [self] {});
 
-    if (m_onError)
+    if (m_onError) {
       m_onError("Hostname resolution timed out");
-  }
-
-  iterator
-  selectAddress(iterator it) const
-  {
-    while (it != iterator() && !m_addressSelector(it->endpoint().address())) {
-      ++it;
     }
-    return it;
   }
 
 private:
-  protocol::resolver m_resolver;
+  boost::asio::ip::udp::resolver m_resolver;
 
   AddressSelector m_addressSelector;
   SuccessCallback m_onSuccess;
@@ -134,28 +116,13 @@
 asyncResolve(const std::string& host,
              const SuccessCallback& onSuccess,
              const ErrorCallback& onError,
-             boost::asio::io_service& ioService,
+             boost::asio::io_context& ioCtx,
              const AddressSelector& addressSelector,
              time::nanoseconds timeout)
 {
-  auto resolver = make_shared<Resolver>(ioService, addressSelector);
-  resolver->asyncResolve(Resolver::query(host, ""), onSuccess, onError, timeout, resolver);
-  // resolver will be destroyed when async operation finishes or ioService stops
-}
-
-IpAddress
-syncResolve(const std::string& host,
-            boost::asio::io_service& ioService,
-            const AddressSelector& addressSelector)
-{
-  Resolver resolver(ioService, addressSelector);
-  auto it = resolver.syncResolve(Resolver::query(host, ""));
-
-  if (it == Resolver::iterator()) {
-    NDN_THROW(Error("No endpoints match the specified address selector"));
-  }
-
-  return it->endpoint().address();
+  auto resolver = make_shared<Resolver>(ioCtx, addressSelector);
+  resolver->asyncResolve(host, onSuccess, onError, timeout, resolver);
+  // resolver will be destroyed when async operation finishes or ioCtx stops
 }
 
 } // namespace ndn::dns
diff --git a/ndn-cxx/net/dns.hpp b/ndn-cxx/net/dns.hpp
index 4859d98..482484d 100644
--- a/ndn-cxx/net/dns.hpp
+++ b/ndn-cxx/net/dns.hpp
@@ -29,13 +29,12 @@
 
 namespace ndn::dns {
 
-using IpAddress = boost::asio::ip::address;
-using AddressSelector = std::function<bool(const IpAddress&)>;
+using AddressSelector = std::function<bool(const boost::asio::ip::address&)>;
 
 struct AnyAddress
 {
   bool
-  operator()(const IpAddress& address) const
+  operator()(const boost::asio::ip::address&) const
   {
     return true;
   }
@@ -44,7 +43,7 @@
 struct Ipv4Only
 {
   bool
-  operator()(const IpAddress& address) const
+  operator()(const boost::asio::ip::address& address) const
   {
     return address.is_v4();
   }
@@ -53,22 +52,17 @@
 struct Ipv6Only
 {
   bool
-  operator()(const IpAddress& address) const
+  operator()(const boost::asio::ip::address& address) const
   {
     return address.is_v6();
   }
 };
 
-class Error : public std::runtime_error
-{
-public:
-  using std::runtime_error::runtime_error;
-};
-
-using SuccessCallback = std::function<void(const IpAddress& address)>;
+using SuccessCallback = std::function<void(const boost::asio::ip::address& address)>;
 using ErrorCallback = std::function<void(const std::string& reason)>;
 
-/** \brief Asynchronously resolve host
+/**
+ * \brief Asynchronously resolve \p host.
  *
  * If an address selector predicate is specified, then each resolved IP address
  * is checked against the predicate.
@@ -80,34 +74,18 @@
  * - dns::Ipv6Address()
  *
  * \warning Even after the DNS resolution has timed out, it's possible that
- *          \p ioService keeps running and \p onSuccess is invoked at a later time.
+ *          \p ioCtx keeps running and \p onSuccess is invoked at a later time.
  *          This could cause segmentation fault if \p onSuccess is deallocated.
- *          To stop the io_service, explicitly invoke \p ioService.stop().
+ *          To stop the io_context, explicitly invoke \p ioCtx.stop().
  */
 void
 asyncResolve(const std::string& host,
              const SuccessCallback& onSuccess,
              const ErrorCallback& onError,
-             boost::asio::io_service& ioService,
+             boost::asio::io_context& ioCtx,
              const AddressSelector& addressSelector = AnyAddress(),
              time::nanoseconds timeout = 4_s);
 
-/** \brief Synchronously resolve host
- *
- * If an address selector predicate is specified, then each resolved IP address
- * is checked against the predicate.
- *
- * Available address selector predicates:
- *
- * - dns::AnyAddress()
- * - dns::Ipv4Address()
- * - dns::Ipv6Address()
- */
-IpAddress
-syncResolve(const std::string& host,
-            boost::asio::io_service& ioService,
-            const AddressSelector& addressSelector = AnyAddress());
-
 } // namespace ndn::dns
 
 #endif // NDN_CXX_NET_DNS_HPP
diff --git a/ndn-cxx/net/face-uri.cpp b/ndn-cxx/net/face-uri.cpp
index da5905f..93cbec4 100644
--- a/ndn-cxx/net/face-uri.cpp
+++ b/ndn-cxx/net/face-uri.cpp
@@ -220,7 +220,7 @@
   canonize(const FaceUri& faceUri,
            const FaceUri::CanonizeSuccessCallback& onSuccess,
            const FaceUri::CanonizeFailureCallback& onFailure,
-           boost::asio::io_service& io, time::nanoseconds timeout) const = 0;
+           boost::asio::io_context& io, time::nanoseconds timeout) const = 0;
 };
 
 template<typename Protocol>
@@ -244,7 +244,7 @@
     }
 
     boost::system::error_code ec;
-    auto addr = boost::asio::ip::address::from_string(unescapeHost(faceUri.getHost()), ec);
+    auto addr = boost::asio::ip::make_address(unescapeHost(faceUri.getHost()), ec);
     if (ec) {
       return false;
     }
@@ -287,7 +287,7 @@
   canonize(const FaceUri& faceUri,
            const FaceUri::CanonizeSuccessCallback& onSuccess,
            const FaceUri::CanonizeFailureCallback& onFailure,
-           boost::asio::io_service& io, time::nanoseconds timeout) const override
+           boost::asio::io_context& io, time::nanoseconds timeout) const override
   {
     if (this->isCanonical(faceUri)) {
       onSuccess(faceUri);
@@ -297,7 +297,7 @@
     // make a copy because caller may modify faceUri
     auto uri = make_shared<FaceUri>(faceUri);
     boost::system::error_code ec;
-    auto ipAddress = boost::asio::ip::address::from_string(unescapeHost(faceUri.getHost()), ec);
+    auto ipAddress = boost::asio::ip::make_address(unescapeHost(faceUri.getHost()), ec);
     if (!ec) {
       // No need to resolve IP address if host is already an IP
       if ((faceUri.getScheme() == m_v4Scheme && !ipAddress.is_v4()) ||
@@ -345,7 +345,7 @@
   onDnsSuccess(const shared_ptr<FaceUri>& faceUri,
                const FaceUri::CanonizeSuccessCallback& onSuccess,
                const FaceUri::CanonizeFailureCallback& onFailure,
-               const dns::IpAddress& ipAddress) const
+               const boost::asio::ip::address& ipAddress) const
   {
     auto [isOk, reason] = this->checkAddress(ipAddress);
     if (!isOk) {
@@ -383,7 +383,7 @@
    *          (false,reason) if the address is not allowable.
    */
   virtual std::pair<bool, std::string>
-  checkAddress(const dns::IpAddress&) const
+  checkAddress(const boost::asio::ip::address&) const
   {
     return {true, ""};
   }
@@ -425,7 +425,7 @@
 
 protected:
   std::pair<bool, std::string>
-  checkAddress(const dns::IpAddress& ipAddress) const override
+  checkAddress(const boost::asio::ip::address& ipAddress) const override
   {
     if (ipAddress.is_multicast()) {
       return {false, "cannot use multicast address"};
@@ -461,7 +461,7 @@
   canonize(const FaceUri& faceUri,
            const FaceUri::CanonizeSuccessCallback& onSuccess,
            const FaceUri::CanonizeFailureCallback& onFailure,
-           boost::asio::io_service&, time::nanoseconds timeout) const override
+           boost::asio::io_context&, time::nanoseconds timeout) const override
   {
     auto addr = ethernet::Address::fromString(faceUri.getHost());
     if (addr.isNull()) {
@@ -493,7 +493,7 @@
   canonize(const FaceUri& faceUri,
            const FaceUri::CanonizeSuccessCallback& onSuccess,
            const FaceUri::CanonizeFailureCallback& onFailure,
-           boost::asio::io_service&, time::nanoseconds timeout) const override
+           boost::asio::io_context&, time::nanoseconds timeout) const override
   {
     if (faceUri.getHost().empty()) {
       onFailure("network interface name is missing");
@@ -539,7 +539,7 @@
   canonize(const FaceUri& faceUri,
            const FaceUri::CanonizeSuccessCallback& onSuccess,
            const FaceUri::CanonizeFailureCallback& onFailure,
-           boost::asio::io_service&, time::nanoseconds timeout) const override
+           boost::asio::io_context&, time::nanoseconds timeout) const override
   {
     if (this->isCanonical(faceUri)) {
       onSuccess(faceUri);
@@ -618,7 +618,7 @@
 void
 FaceUri::canonize(const CanonizeSuccessCallback& onSuccess,
                   const CanonizeFailureCallback& onFailure,
-                  boost::asio::io_service& io, time::nanoseconds timeout) const
+                  boost::asio::io_context& io, time::nanoseconds timeout) const
 {
   const CanonizeProvider* cp = getCanonizeProvider(this->getScheme());
   if (cp == nullptr) {
diff --git a/ndn-cxx/net/face-uri.hpp b/ndn-cxx/net/face-uri.hpp
index 2adbc38..c486dc0 100644
--- a/ndn-cxx/net/face-uri.hpp
+++ b/ndn-cxx/net/face-uri.hpp
@@ -167,7 +167,7 @@
    *
    *  \param onSuccess function to call after this FaceUri is converted to canonical form
    *  \param onFailure function to call if this FaceUri cannot be converted to canonical form
-   *  \param io        reference to `boost::asio::io_service` instance
+   *  \param io        reference to `boost::asio::io_context` instance
    *  \param timeout   maximum allowable duration of the operations.
    *                   It's intentional not to provide a default value: the caller should set
    *                   a reasonable value in balance between network delay and user experience.
@@ -175,7 +175,7 @@
   void
   canonize(const CanonizeSuccessCallback& onSuccess,
            const CanonizeFailureCallback& onFailure,
-           boost::asio::io_service& io,
+           boost::asio::io_context& io,
            time::nanoseconds timeout) const;
 
 private:
diff --git a/ndn-cxx/net/impl/netlink-socket.cpp b/ndn-cxx/net/impl/netlink-socket.cpp
index 0c26637..bfcd4d8 100644
--- a/ndn-cxx/net/impl/netlink-socket.cpp
+++ b/ndn-cxx/net/impl/netlink-socket.cpp
@@ -86,7 +86,7 @@
   int m_value;
 };
 
-NetlinkSocket::NetlinkSocket(boost::asio::io_service& io)
+NetlinkSocket::NetlinkSocket(boost::asio::io_context& io)
   : m_sock(make_shared<boost::asio::generic::raw_protocol::socket>(io))
   , m_pid(0)
   , m_seqNum(static_cast<uint32_t>(time::system_clock::now().time_since_epoch().count()))
@@ -340,7 +340,7 @@
   }
 }
 
-RtnlSocket::RtnlSocket(boost::asio::io_service& io)
+RtnlSocket::RtnlSocket(boost::asio::io_context& io)
   : NetlinkSocket(io)
 {
 }
@@ -409,7 +409,7 @@
 #undef RTM_STRINGIFY
 }
 
-GenlSocket::GenlSocket(boost::asio::io_service& io)
+GenlSocket::GenlSocket(boost::asio::io_context& io)
   : NetlinkSocket(io)
 {
   m_cachedFamilyIds["nlctrl"] = GENL_ID_CTRL;
diff --git a/ndn-cxx/net/impl/netlink-socket.hpp b/ndn-cxx/net/impl/netlink-socket.hpp
index 34a5e9e..481c633 100644
--- a/ndn-cxx/net/impl/netlink-socket.hpp
+++ b/ndn-cxx/net/impl/netlink-socket.hpp
@@ -53,7 +53,7 @@
 
 protected:
   explicit
-  NetlinkSocket(boost::asio::io_service& io);
+  NetlinkSocket(boost::asio::io_context& io);
 
   ~NetlinkSocket();
 
@@ -87,7 +87,7 @@
 {
 public:
   explicit
-  RtnlSocket(boost::asio::io_service& io);
+  RtnlSocket(boost::asio::io_context& io);
 
   void
   open();
@@ -128,7 +128,7 @@
 {
 public:
   explicit
-  GenlSocket(boost::asio::io_service& io);
+  GenlSocket(boost::asio::io_context& io);
 
   void
   open();
diff --git a/ndn-cxx/net/impl/network-monitor-impl-netlink.cpp b/ndn-cxx/net/impl/network-monitor-impl-netlink.cpp
index ca1d749..ec75176 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-netlink.cpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-netlink.cpp
@@ -43,7 +43,7 @@
 
 NDN_LOG_INIT(ndn.NetworkMonitor);
 
-NetworkMonitorImplNetlink::NetworkMonitorImplNetlink(boost::asio::io_service& io)
+NetworkMonitorImplNetlink::NetworkMonitorImplNetlink(boost::asio::io_context& io)
   : m_rtnlSocket(io)
   , m_genlSocket(io)
 {
diff --git a/ndn-cxx/net/impl/network-monitor-impl-netlink.hpp b/ndn-cxx/net/impl/network-monitor-impl-netlink.hpp
index 64616a6..f181546 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-netlink.hpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-netlink.hpp
@@ -44,7 +44,7 @@
    * \brief Initialize netlink socket and start enumerating interfaces.
    */
   explicit
-  NetworkMonitorImplNetlink(boost::asio::io_service& io);
+  NetworkMonitorImplNetlink(boost::asio::io_context& io);
 
   uint32_t
   getCapabilities() const final
diff --git a/ndn-cxx/net/impl/network-monitor-impl-noop.hpp b/ndn-cxx/net/impl/network-monitor-impl-noop.hpp
index f2f2c41..51c4550 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-noop.hpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-noop.hpp
@@ -32,7 +32,7 @@
 {
 public:
   explicit
-  NetworkMonitorImplNoop(boost::asio::io_service&)
+  NetworkMonitorImplNoop(boost::asio::io_context&)
   {
   }
 
diff --git a/ndn-cxx/net/impl/network-monitor-impl-osx.cpp b/ndn-cxx/net/impl/network-monitor-impl-osx.cpp
index 44378d5..ab32dc4 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-osx.cpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-osx.cpp
@@ -62,9 +62,9 @@
 #include <net/if_types.h> // for IFT_* constants
 #include <netinet/in.h>   // for struct sockaddr_in{,6}
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 #include <boost/asio/ip/address.hpp>
-#include <boost/asio/ip/udp.hpp>
+#include <boost/asio/post.hpp>
 #include <boost/range/adaptor/map.hpp>
 #include <boost/range/algorithm_ext/push_back.hpp>
 
@@ -101,7 +101,7 @@
   ifaddrs* m_ifaList = nullptr;
 };
 
-NetworkMonitorImplOsx::NetworkMonitorImplOsx(boost::asio::io_service& io)
+NetworkMonitorImplOsx::NetworkMonitorImplOsx(boost::asio::io_context& io)
   : m_scheduler(io)
   , m_context{0, this, nullptr, nullptr, nullptr}
   , m_scStore(SCDynamicStoreCreate(nullptr, CFSTR("net.named-data.ndn-cxx.NetworkMonitor"),
@@ -142,7 +142,7 @@
     NDN_THROW(Error("SCDynamicStoreSetNotificationKeys failed"));
   }
 
-  io.post([this] { enumerateInterfaces(); });
+  boost::asio::post(io, [this] { enumerateInterfaces(); });
 }
 
 NetworkMonitorImplOsx::~NetworkMonitorImplOsx()
@@ -386,7 +386,7 @@
     if (ipAddr.is_loopback()) {
       scope = AddressScope::HOST;
     }
-    else if ((ipAddr.is_v4() && (ipAddr.to_v4().to_ulong() & 0xFFFF0000) == 0xA9FE0000) ||
+    else if ((ipAddr.is_v4() && (ipAddr.to_v4().to_uint() & 0xFFFF0000) == 0xA9FE0000) ||
              (ipAddr.is_v6() && ipAddr.to_v6().is_link_local())) {
       scope = AddressScope::LINK;
     }
diff --git a/ndn-cxx/net/impl/network-monitor-impl-osx.hpp b/ndn-cxx/net/impl/network-monitor-impl-osx.hpp
index f1866ca..fb3301a 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-osx.hpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-osx.hpp
@@ -45,7 +45,7 @@
 class NetworkMonitorImplOsx final : public NetworkMonitorImpl
 {
 public:
-  NetworkMonitorImplOsx(boost::asio::io_service& io);
+  NetworkMonitorImplOsx(boost::asio::io_context& io);
 
   ~NetworkMonitorImplOsx() final;
 
diff --git a/ndn-cxx/net/network-monitor.cpp b/ndn-cxx/net/network-monitor.cpp
index 79e601f..6d2c365 100644
--- a/ndn-cxx/net/network-monitor.cpp
+++ b/ndn-cxx/net/network-monitor.cpp
@@ -42,7 +42,7 @@
 NDN_LOG_INIT(ndn.NetworkMonitor);
 
 static unique_ptr<NetworkMonitorImpl>
-makeNetworkMonitorImpl(boost::asio::io_service& io)
+makeNetworkMonitorImpl(boost::asio::io_context& io)
 {
   try {
     return make_unique<NETWORK_MONITOR_IMPL_TYPE>(io);
@@ -54,7 +54,7 @@
   }
 }
 
-NetworkMonitor::NetworkMonitor(boost::asio::io_service& io)
+NetworkMonitor::NetworkMonitor(boost::asio::io_context& io)
   : NetworkMonitor(makeNetworkMonitorImpl(io))
 {
 }
diff --git a/ndn-cxx/net/network-monitor.hpp b/ndn-cxx/net/network-monitor.hpp
index f45c4d5..1dcce41 100644
--- a/ndn-cxx/net/network-monitor.hpp
+++ b/ndn-cxx/net/network-monitor.hpp
@@ -60,10 +60,10 @@
   /**
    * @brief Construct instance, request enumeration of all network interfaces, and start
    *        monitoring for network state changes.
-   * @param io io_service instance that will dispatch events
+   * @param ioCtx io_context instance that will dispatch events
    */
   explicit
-  NetworkMonitor(boost::asio::io_service& io);
+  NetworkMonitor(boost::asio::io_context& ioCtx);
 
   enum Capability : uint32_t {
     /// NetworkMonitor is not supported and is a no-op
diff --git a/ndn-cxx/security/certificate-bundle-fetcher.cpp b/ndn-cxx/security/certificate-bundle-fetcher.cpp
index 1179bf0..36bc4bc 100644
--- a/ndn-cxx/security/certificate-bundle-fetcher.cpp
+++ b/ndn-cxx/security/certificate-bundle-fetcher.cpp
@@ -67,7 +67,7 @@
                                   const shared_ptr<ValidationState>& state,
                                   const ValidationContinuation& continueValidation)
 {
-  auto dataValidationState = dynamic_pointer_cast<DataValidationState>(state);
+  auto dataValidationState = std::dynamic_pointer_cast<DataValidationState>(state);
   if (dataValidationState == nullptr) {
     return m_inner->fetch(certRequest, state, continueValidation);
   }
diff --git a/ndn-cxx/security/certificate-fetcher-direct-fetch.cpp b/ndn-cxx/security/certificate-fetcher-direct-fetch.cpp
index c7df4f2..3871037 100644
--- a/ndn-cxx/security/certificate-fetcher-direct-fetch.cpp
+++ b/ndn-cxx/security/certificate-fetcher-direct-fetch.cpp
@@ -45,7 +45,7 @@
                                        const ValidationContinuation& continueValidation)
 {
   uint64_t incomingFaceId = 0;
-  auto interestState = dynamic_pointer_cast<InterestValidationState>(state);
+  auto interestState = std::dynamic_pointer_cast<InterestValidationState>(state);
   if (interestState != nullptr) {
     auto incomingFaceIdTag = interestState->getOriginalInterest().getTag<lp::IncomingFaceIdTag>();
     if (incomingFaceIdTag != nullptr) {
@@ -53,7 +53,7 @@
     }
   }
   else {
-    auto dataState = dynamic_pointer_cast<DataValidationState>(state);
+    auto dataState = std::dynamic_pointer_cast<DataValidationState>(state);
     auto incomingFaceIdTag = dataState->getOriginalData().getTag<lp::IncomingFaceIdTag>();
     if (incomingFaceIdTag != nullptr) {
       incomingFaceId = incomingFaceIdTag->get();
diff --git a/ndn-cxx/security/certificate-fetcher-from-network.cpp b/ndn-cxx/security/certificate-fetcher-from-network.cpp
index 0861c0d..8ec9113 100644
--- a/ndn-cxx/security/certificate-fetcher-from-network.cpp
+++ b/ndn-cxx/security/certificate-fetcher-from-network.cpp
@@ -35,7 +35,7 @@
 
 CertificateFetcherFromNetwork::CertificateFetcherFromNetwork(Face& face)
   : m_face(face)
-  , m_scheduler(face.getIoService())
+  , m_scheduler(face.getIoContext())
 {
 }
 
diff --git a/ndn-cxx/security/validation-policy-command-interest.cpp b/ndn-cxx/security/validation-policy-command-interest.cpp
index ddae489..b2fbe8f 100644
--- a/ndn-cxx/security/validation-policy-command-interest.cpp
+++ b/ndn-cxx/security/validation-policy-command-interest.cpp
@@ -146,7 +146,7 @@
     }
   }
 
-  auto interestState = dynamic_pointer_cast<InterestValidationState>(state);
+  auto interestState = std::dynamic_pointer_cast<InterestValidationState>(state);
   BOOST_ASSERT(interestState != nullptr);
   interestState->afterSuccess.connect([=] (const Interest&) { insertNewRecord(keyName, timestamp); });
   return true;
diff --git a/ndn-cxx/security/validation-policy-signed-interest.cpp b/ndn-cxx/security/validation-policy-signed-interest.cpp
index 58f5779..e56d3a8 100644
--- a/ndn-cxx/security/validation-policy-signed-interest.cpp
+++ b/ndn-cxx/security/validation-policy-signed-interest.cpp
@@ -129,7 +129,7 @@
   }
 
   if (m_options.maxRecordCount != 0) {
-    auto interestState = dynamic_pointer_cast<InterestValidationState>(state);
+    auto interestState = std::dynamic_pointer_cast<InterestValidationState>(state);
     BOOST_ASSERT(interestState != nullptr);
     interestState->afterSuccess.connect([=] (const Interest&) {
       insertRecord(keyName, timestamp, seqNum, nonce);
diff --git a/ndn-cxx/transport/detail/stream-transport-impl.hpp b/ndn-cxx/transport/detail/stream-transport-impl.hpp
index 0ee1035..ff10539 100644
--- a/ndn-cxx/transport/detail/stream-transport-impl.hpp
+++ b/ndn-cxx/transport/detail/stream-transport-impl.hpp
@@ -44,10 +44,10 @@
   using Impl = StreamTransportImpl<BaseTransport, Protocol>;
   using TransmissionQueue = std::queue<Block, std::list<Block>>;
 
-  StreamTransportImpl(BaseTransport& transport, boost::asio::io_service& ioService)
+  StreamTransportImpl(BaseTransport& transport, boost::asio::io_context& ioCtx)
     : m_transport(transport)
-    , m_socket(ioService)
-    , m_connectTimer(ioService)
+    , m_socket(ioCtx)
+    , m_connectTimer(ioCtx)
   {
   }
 
@@ -61,7 +61,7 @@
 
     // Wait at most 4 seconds to connect
     /// @todo Decide whether this number should be configurable
-    m_connectTimer.expires_from_now(std::chrono::seconds(4));
+    m_connectTimer.expires_after(std::chrono::seconds(4));
     m_connectTimer.async_wait([self = this->shared_from_this()] (const auto& error) {
       self->connectTimeoutHandler(error);
     });
@@ -77,8 +77,8 @@
   {
     m_transport.setState(Transport::State::CLOSED);
 
+    m_connectTimer.cancel();
     boost::system::error_code error; // to silently ignore all errors
-    m_connectTimer.cancel(error);
     m_socket.cancel(error);
     m_socket.close(error);
 
diff --git a/ndn-cxx/transport/detail/stream-transport-with-resolver-impl.hpp b/ndn-cxx/transport/detail/stream-transport-with-resolver-impl.hpp
index 2ca01e4..dec886a 100644
--- a/ndn-cxx/transport/detail/stream-transport-with-resolver-impl.hpp
+++ b/ndn-cxx/transport/detail/stream-transport-with-resolver-impl.hpp
@@ -33,13 +33,13 @@
 class StreamTransportWithResolverImpl : public StreamTransportImpl<BaseTransport, Protocol>
 {
 public:
-  StreamTransportWithResolverImpl(BaseTransport& transport, boost::asio::io_service& ioService)
-    : StreamTransportImpl<BaseTransport, Protocol>(transport, ioService)
+  StreamTransportWithResolverImpl(BaseTransport& transport, boost::asio::io_context& ioCtx)
+    : StreamTransportImpl<BaseTransport, Protocol>(transport, ioCtx)
   {
   }
 
   void
-  connect(const typename Protocol::resolver::query& query)
+  connect(std::string_view host, std::string_view port)
   {
     if (this->m_transport.getState() == Transport::State::CONNECTING) {
       return;
@@ -48,13 +48,13 @@
 
     // Wait at most 4 seconds to connect
     /// @todo Decide whether this number should be configurable
-    this->m_connectTimer.expires_from_now(std::chrono::seconds(4));
+    this->m_connectTimer.expires_after(std::chrono::seconds(4));
     this->m_connectTimer.async_wait([self = this->shared_from_base()] (const auto& ec) {
       self->connectTimeoutHandler(ec);
     });
 
     auto resolver = make_shared<typename Protocol::resolver>(this->m_socket.get_executor());
-    resolver->async_resolve(query, [self = this->shared_from_base(), resolver] (auto&&... args) {
+    resolver->async_resolve(host, port, [self = this->shared_from_base(), resolver] (auto&&... args) {
       self->resolveHandler(std::forward<decltype(args)>(args)..., resolver);
     });
   }
@@ -62,23 +62,20 @@
 protected:
   void
   resolveHandler(const boost::system::error_code& error,
-                 typename Protocol::resolver::iterator endpoint,
+                 const typename Protocol::resolver::results_type& endpoints,
                  const shared_ptr<typename Protocol::resolver>&)
   {
     if (error) {
       if (error == boost::system::errc::operation_canceled)
         return;
 
-      NDN_THROW(Transport::Error(error, "Error during resolution of host or port"));
-    }
-
-    typename Protocol::resolver::iterator end;
-    if (endpoint == end) {
       this->m_transport.close();
-      NDN_THROW(Transport::Error(error, "Unable to resolve host or port"));
+      NDN_THROW(Transport::Error(error, "unable to resolve host or port"));
     }
 
-    this->m_socket.async_connect(*endpoint, [self = this->shared_from_base()] (const auto& ec) {
+    BOOST_ASSERT(!endpoints.empty()); // guaranteed by Asio if the resolve operation is successful
+
+    this->m_socket.async_connect(*endpoints.begin(), [self = this->shared_from_base()] (const auto& ec) {
       self->connectHandler(ec);
     });
   }
@@ -89,7 +86,7 @@
   shared_ptr<Impl>
   shared_from_base()
   {
-    return static_pointer_cast<Impl>(this->shared_from_this());
+    return std::static_pointer_cast<Impl>(this->shared_from_this());
   }
 };
 
diff --git a/ndn-cxx/transport/tcp-transport.cpp b/ndn-cxx/transport/tcp-transport.cpp
index 20360ef..79f53fa 100644
--- a/ndn-cxx/transport/tcp-transport.cpp
+++ b/ndn-cxx/transport/tcp-transport.cpp
@@ -78,17 +78,15 @@
 }
 
 void
-TcpTransport::connect(boost::asio::io_service& ioService, ReceiveCallback receiveCallback)
+TcpTransport::connect(boost::asio::io_context& ioCtx, ReceiveCallback receiveCallback)
 {
   NDN_LOG_DEBUG("connect host=" << m_host << " port=" << m_port);
 
   if (m_impl == nullptr) {
-    Transport::connect(ioService, std::move(receiveCallback));
-    m_impl = make_shared<Impl>(*this, ioService);
+    Transport::connect(ioCtx, std::move(receiveCallback));
+    m_impl = make_shared<Impl>(*this, ioCtx);
   }
-
-  boost::asio::ip::tcp::resolver::query query(m_host, m_port);
-  m_impl->connect(query);
+  m_impl->connect(m_host, m_port);
 }
 
 void
diff --git a/ndn-cxx/transport/tcp-transport.hpp b/ndn-cxx/transport/tcp-transport.hpp
index a40ba0f..042fe0b 100644
--- a/ndn-cxx/transport/tcp-transport.hpp
+++ b/ndn-cxx/transport/tcp-transport.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2022 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -38,7 +38,8 @@
 
 } // namespace detail
 
-/** \brief A transport using TCP socket.
+/**
+ * \brief A transport that uses a TCP socket for communication.
  */
 class TcpTransport : public Transport
 {
@@ -49,7 +50,7 @@
   ~TcpTransport() override;
 
   void
-  connect(boost::asio::io_service& ioService, ReceiveCallback receiveCallback) override;
+  connect(boost::asio::io_context& ioCtx, ReceiveCallback receiveCallback) override;
 
   void
   close() override;
@@ -63,8 +64,9 @@
   void
   send(const Block& wire) override;
 
-  /** \brief Create transport with parameters defined in URI.
-   *  \throw Transport::Error incorrect URI or unsupported protocol is specified
+  /**
+   * \brief Create transport with parameters defined in URI.
+   * \throw Transport::Error incorrect URI or unsupported protocol is specified
    */
   static shared_ptr<TcpTransport>
   create(const std::string& uri);
diff --git a/ndn-cxx/transport/transport.cpp b/ndn-cxx/transport/transport.cpp
index a78e36f..1b7c774 100644
--- a/ndn-cxx/transport/transport.cpp
+++ b/ndn-cxx/transport/transport.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -29,11 +29,11 @@
 }
 
 void
-Transport::connect(boost::asio::io_service& ioService, ReceiveCallback receiveCallback)
+Transport::connect(boost::asio::io_context& ioCtx, ReceiveCallback receiveCallback)
 {
   BOOST_ASSERT(receiveCallback != nullptr);
 
-  m_ioService = &ioService;
+  m_ioCtx = &ioCtx;
   m_receiveCallback = std::move(receiveCallback);
 }
 
diff --git a/ndn-cxx/transport/transport.hpp b/ndn-cxx/transport/transport.hpp
index 2ef4b3d..b6ccdb4 100644
--- a/ndn-cxx/transport/transport.hpp
+++ b/ndn-cxx/transport/transport.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2022 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -59,12 +59,12 @@
 
   /**
    * \brief Asynchronously open the connection.
-   * \param ioService io_service to create socket on
+   * \param ioCtx io_context to create socket on
    * \param receiveCallback callback function when a TLV block is received; must not be empty
    * \throw boost::system::system_error connection cannot be established
    */
   virtual void
-  connect(boost::asio::io_service& ioService, ReceiveCallback receiveCallback);
+  connect(boost::asio::io_context& ioCtx, ReceiveCallback receiveCallback);
 
   /**
    * \brief Close the connection.
@@ -113,7 +113,7 @@
   }
 
 protected:
-  boost::asio::io_service* m_ioService = nullptr;
+  boost::asio::io_context* m_ioCtx = nullptr;
   ReceiveCallback m_receiveCallback;
 
 private:
diff --git a/ndn-cxx/transport/unix-transport.cpp b/ndn-cxx/transport/unix-transport.cpp
index 110423a..33f3919 100644
--- a/ndn-cxx/transport/unix-transport.cpp
+++ b/ndn-cxx/transport/unix-transport.cpp
@@ -76,13 +76,13 @@
 }
 
 void
-UnixTransport::connect(boost::asio::io_service& ioService, ReceiveCallback receiveCallback)
+UnixTransport::connect(boost::asio::io_context& ioCtx, ReceiveCallback receiveCallback)
 {
   NDN_LOG_DEBUG("connect path=" << m_unixSocket);
 
   if (m_impl == nullptr) {
-    Transport::connect(ioService, std::move(receiveCallback));
-    m_impl = make_shared<Impl>(*this, ioService);
+    Transport::connect(ioCtx, std::move(receiveCallback));
+    m_impl = make_shared<Impl>(*this, ioCtx);
   }
 
   m_impl->connect(boost::asio::local::stream_protocol::endpoint(m_unixSocket));
diff --git a/ndn-cxx/transport/unix-transport.hpp b/ndn-cxx/transport/unix-transport.hpp
index 4a31a59..91c2f71 100644
--- a/ndn-cxx/transport/unix-transport.hpp
+++ b/ndn-cxx/transport/unix-transport.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2022 Regents of the University of California.
+ * Copyright (c) 2013-2023 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -35,7 +35,8 @@
 
 } // namespace detail
 
-/** \brief A transport using Unix stream socket.
+/**
+ * \brief A transport that uses a Unix stream socket for communication.
  */
 class UnixTransport : public Transport
 {
@@ -46,7 +47,7 @@
   ~UnixTransport() override;
 
   void
-  connect(boost::asio::io_service& ioService, ReceiveCallback receiveCallback) override;
+  connect(boost::asio::io_context& ioCtx, ReceiveCallback receiveCallback) override;
 
   void
   close() override;
@@ -60,8 +61,9 @@
   void
   send(const Block& wire) override;
 
-  /** \brief Create transport with parameters defined in URI.
-   *  \throw Transport::Error incorrect URI or unsupported protocol is specified
+  /**
+   * \brief Create transport with parameters defined in URI.
+   * \throw Transport::Error incorrect URI or unsupported protocol is specified
    */
   static shared_ptr<UnixTransport>
   create(const std::string& uri);
diff --git a/ndn-cxx/util/dummy-client-face.cpp b/ndn-cxx/util/dummy-client-face.cpp
index 1f278e5..06981b9 100644
--- a/ndn-cxx/util/dummy-client-face.cpp
+++ b/ndn-cxx/util/dummy-client-face.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "ndn-cxx/util/dummy-client-face.hpp"
+
 #include "ndn-cxx/impl/lp-field-tag.hpp"
 #include "ndn-cxx/lp/packet.hpp"
 #include "ndn-cxx/lp/tags.hpp"
@@ -27,7 +28,8 @@
 #include "ndn-cxx/mgmt/nfd/control-response.hpp"
 #include "ndn-cxx/transport/transport.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
 
 namespace ndn {
 
@@ -93,16 +95,16 @@
   this->construct(options);
 }
 
-DummyClientFace::DummyClientFace(boost::asio::io_service& ioService, const Options& options)
-  : Face(make_shared<DummyClientFace::Transport>(), ioService)
+DummyClientFace::DummyClientFace(boost::asio::io_context& ioCtx, const Options& options)
+  : Face(make_shared<DummyClientFace::Transport>(), ioCtx)
   , m_internalKeyChain(make_unique<KeyChain>())
   , m_keyChain(*m_internalKeyChain)
 {
   this->construct(options);
 }
 
-DummyClientFace::DummyClientFace(boost::asio::io_service& ioService, KeyChain& keyChain, const Options& options)
-  : Face(make_shared<DummyClientFace::Transport>(), ioService, keyChain)
+DummyClientFace::DummyClientFace(boost::asio::io_context& ioCtx, KeyChain& keyChain, const Options& options)
+  : Face(make_shared<DummyClientFace::Transport>(), ioCtx, keyChain)
   , m_keyChain(keyChain)
 {
   this->construct(options);
@@ -116,7 +118,7 @@
 void
 DummyClientFace::construct(const Options& options)
 {
-  static_pointer_cast<Transport>(getTransport())->onSendBlock.connect([this] (Block packet) {
+  static_cast<Transport&>(getTransport()).onSendBlock.connect([this] (Block packet) {
     packet.encode();
     lp::Packet lpPacket(packet);
     auto frag = lpPacket.get<lp::FragmentField>();
@@ -145,10 +147,10 @@
   });
 
   if (options.enablePacketLogging)
-    this->enablePacketLogging();
+    enablePacketLogging();
 
   if (options.enableRegistrationReply)
-    this->enableRegistrationReply(options.registrationReplyFaceId);
+    enableRegistrationReply(options.registrationReplyFaceId);
 
   m_processEventsOverride = options.processEventsOverride;
 
@@ -226,10 +228,10 @@
     resp.setCode(200);
     resp.setBody(params.wireEncode());
 
-    shared_ptr<Data> data = make_shared<Data>(name);
+    auto data = make_shared<Data>(name);
     data->setContent(resp.wireEncode());
     m_keyChain.sign(*data, security::SigningInfo(security::SigningInfo::SIGNER_TYPE_SHA256));
-    this->getIoService().post([this, data] { this->receive(*data); });
+    boost::asio::post(getIoContext(), [this, data] { this->receive(*data); });
   });
 }
 
@@ -242,7 +244,7 @@
   addFieldFromTag<lp::NextHopFaceIdField, lp::NextHopFaceIdTag>(lpPacket, interest);
   addFieldFromTag<lp::CongestionMarkField, lp::CongestionMarkTag>(lpPacket, interest);
 
-  static_pointer_cast<Transport>(getTransport())->receive(lpPacket.wireEncode());
+  static_cast<Transport&>(getTransport()).receive(lpPacket.wireEncode());
 }
 
 void
@@ -253,7 +255,7 @@
   addFieldFromTag<lp::IncomingFaceIdField, lp::IncomingFaceIdTag>(lpPacket, data);
   addFieldFromTag<lp::CongestionMarkField, lp::CongestionMarkTag>(lpPacket, data);
 
-  static_pointer_cast<Transport>(getTransport())->receive(lpPacket.wireEncode());
+  static_cast<Transport&>(getTransport()).receive(lpPacket.wireEncode());
 }
 
 void
@@ -267,7 +269,7 @@
   addFieldFromTag<lp::IncomingFaceIdField, lp::IncomingFaceIdTag>(lpPacket, nack);
   addFieldFromTag<lp::CongestionMarkField, lp::CongestionMarkTag>(lpPacket, nack);
 
-  static_pointer_cast<Transport>(getTransport())->receive(lpPacket.wireEncode());
+  static_cast<Transport&>(getTransport()).receive(lpPacket.wireEncode());
 }
 
 void
@@ -313,13 +315,13 @@
 }
 
 void
-DummyClientFace::doProcessEvents(time::milliseconds timeout, bool keepThread)
+DummyClientFace::doProcessEvents(time::milliseconds timeout, bool keepRunning)
 {
   if (m_processEventsOverride != nullptr) {
     m_processEventsOverride(timeout);
   }
   else {
-    this->Face::doProcessEvents(timeout, keepThread);
+    Face::doProcessEvents(timeout, keepRunning);
   }
 }
 
diff --git a/ndn-cxx/util/dummy-client-face.hpp b/ndn-cxx/util/dummy-client-face.hpp
index bc92c7e..6583e36 100644
--- a/ndn-cxx/util/dummy-client-face.hpp
+++ b/ndn-cxx/util/dummy-client-face.hpp
@@ -83,24 +83,24 @@
     AlreadyLinkedError();
   };
 
-  /** \brief Create a dummy face with internal IO service.
+  /** \brief Create a dummy face with internal I/O context.
    */
   explicit
   DummyClientFace(const Options& options = Options());
 
-  /** \brief Create a dummy face with internal IO service and the specified KeyChain.
+  /** \brief Create a dummy face with internal I/O context and the specified KeyChain.
    */
   explicit
   DummyClientFace(KeyChain& keyChain, const Options& options = Options());
 
-  /** \brief Create a dummy face with the provided IO service.
+  /** \brief Create a dummy face with the provided I/O context.
    */
   explicit
-  DummyClientFace(boost::asio::io_service& ioService, const Options& options = Options());
+  DummyClientFace(boost::asio::io_context& ioCtx, const Options& options = Options());
 
-  /** \brief Create a dummy face with the provided IO service and the specified KeyChain.
+  /** \brief Create a dummy face with the provided I/O context and the specified KeyChain.
    */
-  DummyClientFace(boost::asio::io_service& ioService, KeyChain& keyChain,
+  DummyClientFace(boost::asio::io_context& ioCtx, KeyChain& keyChain,
                   const Options& options = Options());
 
   ~DummyClientFace() override;
@@ -146,7 +146,7 @@
   enableRegistrationReply(uint64_t faceId);
 
   void
-  doProcessEvents(time::milliseconds timeout, bool keepThread) override;
+  doProcessEvents(time::milliseconds timeout, bool keepRunning) override;
 
 public:
   /** \brief Interests sent out of this DummyClientFace.
diff --git a/ndn-cxx/util/notification-subscriber.cpp b/ndn-cxx/util/notification-subscriber.cpp
index 4a13240..ac934d1 100644
--- a/ndn-cxx/util/notification-subscriber.cpp
+++ b/ndn-cxx/util/notification-subscriber.cpp
@@ -40,7 +40,7 @@
   , m_lastSequenceNum(std::numeric_limits<uint64_t>::max())
   , m_lastNackSequenceNum(std::numeric_limits<uint64_t>::max())
   , m_attempts(1)
-  , m_scheduler(face.getIoService())
+  , m_scheduler(face.getIoContext())
   , m_interestLifetime(interestLifetime)
 {
 }
diff --git a/ndn-cxx/util/scheduler.cpp b/ndn-cxx/util/scheduler.cpp
index fae2a84..8b58e83 100644
--- a/ndn-cxx/util/scheduler.cpp
+++ b/ndn-cxx/util/scheduler.cpp
@@ -25,27 +25,20 @@
 
 namespace ndn::scheduler {
 
-/** \brief Stores internal information about a scheduled event
+/**
+ * \brief Stores internal information about a scheduled event.
  */
-class EventInfo : noncopyable
+struct EventInfo : noncopyable
 {
-public:
   EventInfo(time::nanoseconds after, EventCallback&& cb)
-    : callback(std::move(cb))
-    , expireTime(time::steady_clock::now() + after)
+    : expiry(time::steady_clock::now() + after)
+    , callback(std::move(cb))
   {
   }
 
-  [[nodiscard]] time::nanoseconds
-  expiresFromNow() const
-  {
-    return std::max(expireTime - time::steady_clock::now(), 0_ns);
-  }
-
-public:
+  time::steady_clock::time_point expiry;
   EventCallback callback;
   Scheduler::EventQueue::const_iterator queueIt;
-  time::steady_clock::time_point expireTime;
   bool isExpired = false;
 };
 
@@ -71,11 +64,11 @@
 Scheduler::EventQueueCompare::operator()(const shared_ptr<EventInfo>& a,
                                          const shared_ptr<EventInfo>& b) const noexcept
 {
-  return a->expireTime < b->expireTime;
+  return a->expiry < b->expiry;
 }
 
-Scheduler::Scheduler(boost::asio::io_service& ioService)
-  : m_timer(make_unique<detail::SteadyTimer>(ioService))
+Scheduler::Scheduler(boost::asio::io_context& ioCtx)
+  : m_timer(make_unique<detail::SteadyTimer>(ioCtx))
 {
 }
 
@@ -125,8 +118,8 @@
 Scheduler::scheduleNext()
 {
   if (!m_queue.empty()) {
-    m_timer->expires_from_now((*m_queue.begin())->expiresFromNow());
-    m_timer->async_wait([this] (const auto& error) { this->executeEvent(error); });
+    m_timer->expires_at((*m_queue.begin())->expiry);
+    m_timer->async_wait([this] (const auto& error) { executeEvent(error); });
   }
 }
 
@@ -148,7 +141,7 @@
   while (!m_queue.empty()) {
     auto head = m_queue.begin();
     shared_ptr<EventInfo> info = *head;
-    if (info->expireTime > now) {
+    if (info->expiry > now) {
       break;
     }
 
diff --git a/ndn-cxx/util/scheduler.hpp b/ndn-cxx/util/scheduler.hpp
index e82b151..bdf598d 100644
--- a/ndn-cxx/util/scheduler.hpp
+++ b/ndn-cxx/util/scheduler.hpp
@@ -40,7 +40,7 @@
 namespace scheduler {
 
 class Scheduler;
-class EventInfo;
+struct EventInfo;
 
 /** \brief Function to be invoked when a scheduled event expires
  */
@@ -130,13 +130,14 @@
  */
 using ScopedEventId = detail::ScopedCancelHandle<EventId>;
 
-/** \brief Generic time-based scheduler
+/**
+ * \brief Generic time-based event scheduler.
  */
 class Scheduler : noncopyable
 {
 public:
   explicit
-  Scheduler(boost::asio::io_service& ioService);
+  Scheduler(boost::asio::io_context& ioCtx);
 
   ~Scheduler();
 
@@ -162,7 +163,7 @@
 
   /** \brief Execute expired events
    *
-   *  If an event callback throws, the exception is propagated to the thread running the io_service.
+   *  If an event callback throws, the exception is propagated to the thread running the io_context.
    *  In case there are other expired events, they will be processed in the next invocation.
    */
   void
diff --git a/ndn-cxx/util/segment-fetcher.cpp b/ndn-cxx/util/segment-fetcher.cpp
index b570a7b..11cc339 100644
--- a/ndn-cxx/util/segment-fetcher.cpp
+++ b/ndn-cxx/util/segment-fetcher.cpp
@@ -27,7 +27,8 @@
 #include "ndn-cxx/lp/nack.hpp"
 #include "ndn-cxx/lp/nack-header.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
 #include <boost/lexical_cast.hpp>
 #include <boost/range/adaptor/map.hpp>
 
@@ -61,7 +62,7 @@
                                const SegmentFetcher::Options& options)
   : m_options(options)
   , m_face(face)
-  , m_scheduler(m_face.getIoService())
+  , m_scheduler(m_face.getIoContext())
   , m_validator(validator)
   , m_rttEstimator(make_shared<util::RttEstimator::Options>(options.rttOptions))
   , m_timeLastSegmentReceived(time::steady_clock::now())
@@ -91,7 +92,7 @@
   }
 
   m_pendingSegments.clear(); // cancels pending Interests and timeout events
-  m_face.getIoService().post([self = std::move(m_this)] {});
+  boost::asio::post(m_face.getIoContext(), [self = std::move(m_this)] {});
 }
 
 bool
diff --git a/tests/benchmarks/scheduler-bench.cpp b/tests/benchmarks/scheduler-bench.cpp
index 46af278..e1e3433 100644
--- a/tests/benchmarks/scheduler-bench.cpp
+++ b/tests/benchmarks/scheduler-bench.cpp
@@ -25,14 +25,14 @@
 #include "ndn-cxx/util/scheduler.hpp"
 #include "tests/benchmarks/timed-execute.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 #include <iostream>
 
 namespace ndn::tests {
 
 BOOST_AUTO_TEST_CASE(ScheduleCancel)
 {
-  boost::asio::io_service io;
+  boost::asio::io_context io;
   Scheduler sched(io);
 
   const size_t nEvents = 1000000;
@@ -56,7 +56,7 @@
 
 BOOST_AUTO_TEST_CASE(Execute)
 {
-  boost::asio::io_service io;
+  boost::asio::io_context io;
   Scheduler sched(io);
 
   const size_t nEvents = 1000000;
diff --git a/tests/integration/face.cpp b/tests/integration/face.cpp
index 7bd2408..a759e35 100644
--- a/tests/integration/face.cpp
+++ b/tests/integration/face.cpp
@@ -68,7 +68,7 @@
 protected:
   FaceFixture()
     : face(TransportType::create(""), m_keyChain)
-    , sched(face.getIoService())
+    , sched(face.getIoContext())
   {
   }
 
@@ -82,7 +82,7 @@
   sendInterest(time::nanoseconds delay, const Interest& interest, char& outcome)
   {
     if (face2 == nullptr) {
-      face2 = make_unique<Face>(TransportType::create(""), face.getIoService(), m_keyChain);
+      face2 = make_unique<Face>(TransportType::create(""), face.getIoContext(), m_keyChain);
     }
 
     outcome = '?';
@@ -101,13 +101,13 @@
     return sendInterest(delay, interest, ignoredOutcome);
   }
 
-  /** \brief Stop io_service after a delay
+  /** \brief Stop io_context after a delay
    *  \return scheduled event id
    */
   scheduler::EventId
   terminateAfter(time::nanoseconds delay)
   {
-    return sched.schedule(delay, [this] { face.getIoService().stop(); });
+    return sched.schedule(delay, [this] { face.getIoContext().stop(); });
   }
 
 protected:
diff --git a/tests/integration/network-monitor.cpp b/tests/integration/network-monitor.cpp
index cf6d8bd..ff4d932 100644
--- a/tests/integration/network-monitor.cpp
+++ b/tests/integration/network-monitor.cpp
@@ -30,7 +30,7 @@
 #include "ndn-cxx/util/string-helper.hpp"
 #include "ndn-cxx/util/time.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 #include <iostream>
 
 namespace ndn::tests {
@@ -48,7 +48,7 @@
 
 BOOST_AUTO_TEST_CASE(Signals)
 {
-  boost::asio::io_service io;
+  boost::asio::io_context io;
   NetworkMonitor monitor(io);
 
   std::cout << "capabilities=" << AsHex{monitor.getCapabilities()} << std::endl;
diff --git a/tests/unit/encoding/block.t.cpp b/tests/unit/encoding/block.t.cpp
index c93a307..b4f2cf8 100644
--- a/tests/unit/encoding/block.t.cpp
+++ b/tests/unit/encoding/block.t.cpp
@@ -649,8 +649,8 @@
 {
   Block block = "0101A0"_block;
   boost::asio::const_buffer buffer(block);
-  BOOST_CHECK_EQUAL(boost::asio::buffer_cast<const uint8_t*>(buffer), block.data());
-  BOOST_CHECK_EQUAL(boost::asio::buffer_size(buffer), block.size());
+  BOOST_CHECK_EQUAL(buffer.data(), block.data());
+  BOOST_CHECK_EQUAL(buffer.size(), block.size());
 }
 
 BOOST_AUTO_TEST_CASE(Equality)
diff --git a/tests/unit/face.t.cpp b/tests/unit/face.t.cpp
index f369d6f..78d7c67 100644
--- a/tests/unit/face.t.cpp
+++ b/tests/unit/face.t.cpp
@@ -96,8 +96,8 @@
                          BOOST_CHECK_EQUAL(i.getName(), "/Hello/World");
                          ++nData;
                        },
-                       std::bind([] { BOOST_FAIL("Unexpected Nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   advanceClocks(40_ms);
 
@@ -112,9 +112,9 @@
 
   size_t nTimeouts = 0;
   face.expressInterest(*makeInterest("/Hello/World/a/2", false, 50_ms),
-                       std::bind([]{}),
-                       std::bind([]{}),
-                       std::bind([&nTimeouts] { ++nTimeouts; }));
+                       [] (auto&&...) {},
+                       [] (auto&&...) {},
+                       [&] (auto&&...) { ++nTimeouts; });
   advanceClocks(200_ms, 5);
   BOOST_CHECK_EQUAL(nTimeouts, 1);
 }
@@ -124,14 +124,14 @@
   size_t nData = 0;
 
   face.expressInterest(*makeInterest("/Hello/World", true, 50_ms),
-                       [&] (const auto&, const auto&) { ++nData; },
-                       std::bind([] { BOOST_FAIL("Unexpected Nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [&] (auto&&...) { ++nData; },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   face.expressInterest(*makeInterest("/Hello/World/a", true, 50_ms),
-                       [&] (const auto&, const auto&) { ++nData; },
-                       std::bind([] { BOOST_FAIL("Unexpected Nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [&] (auto&&...) { ++nData; },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   advanceClocks(40_ms);
 
@@ -148,8 +148,8 @@
 {
   face.expressInterest(*makeInterest("/Hello/World", true),
                        nullptr,
-                       std::bind([] { BOOST_FAIL("Unexpected Nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
   advanceClocks(1_ms);
 
   BOOST_CHECK_NO_THROW(do {
@@ -162,13 +162,12 @@
 {
   size_t nTimeouts = 0;
   face.expressInterest(*makeInterest("/Hello/World", false, 50_ms),
-                       std::bind([] { BOOST_FAIL("Unexpected Data"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected Nack"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Data"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Nack"); },
                        [&nTimeouts] (const Interest& i) {
                          BOOST_CHECK_EQUAL(i.getName(), "/Hello/World");
                          ++nTimeouts;
                        });
-
   advanceClocks(200_ms, 5);
 
   BOOST_CHECK_EQUAL(nTimeouts, 1);
@@ -180,8 +179,8 @@
 BOOST_AUTO_TEST_CASE(EmptyTimeoutCallback)
 {
   face.expressInterest(*makeInterest("/Hello/World", false, 50_ms),
-                       std::bind([] { BOOST_FAIL("Unexpected Data"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected Nack"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Data"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Nack"); },
                        nullptr);
   advanceClocks(40_ms);
 
@@ -195,16 +194,15 @@
   size_t nNacks = 0;
 
   auto interest = makeInterest("/Hello/World", false, 50_ms);
-
   face.expressInterest(*interest,
-                       std::bind([] { BOOST_FAIL("Unexpected Data"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Data"); },
                        [&] (const Interest& i, const lp::Nack& n) {
                          BOOST_CHECK(i.getName().isPrefixOf(n.getInterest().getName()));
                          BOOST_CHECK_EQUAL(i.getName(), "/Hello/World");
                          BOOST_CHECK_EQUAL(n.getReason(), lp::NackReason::DUPLICATE);
                          ++nNacks;
                        },
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   advanceClocks(40_ms);
 
@@ -222,15 +220,15 @@
 
   auto interest = makeInterest("/Hello/World", false, 50_ms, 1);
   face.expressInterest(*interest,
-                       std::bind([] { BOOST_FAIL("Unexpected Data"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Data"); },
                        [&] (const auto&, const auto&) { ++nNacks; },
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   interest->setNonce(2);
   face.expressInterest(*interest,
-                       std::bind([] { BOOST_FAIL("Unexpected Data"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Data"); },
                        [&] (const auto&, const auto&) { ++nNacks; },
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   advanceClocks(40_ms);
 
@@ -245,9 +243,9 @@
 BOOST_AUTO_TEST_CASE(EmptyNackCallback)
 {
   face.expressInterest(*makeInterest("/Hello/World"),
-                       std::bind([] { BOOST_FAIL("Unexpected Data"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected Data"); },
                        nullptr,
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
   advanceClocks(1_ms);
 
   BOOST_CHECK_NO_THROW(do {
@@ -259,7 +257,7 @@
 BOOST_AUTO_TEST_CASE(PutDataFromDataCallback) // Bug 4596
 {
   face.expressInterest(*makeInterest("/localhost/notification/1"),
-                       [&] (const auto&, const auto&) {
+                       [&] (auto&&...) {
                          face.put(*makeData("/chronosync/sampleDigest/1"));
                        }, nullptr, nullptr);
   advanceClocks(10_ms);
@@ -290,9 +288,9 @@
 BOOST_AUTO_TEST_CASE(Handle)
 {
   auto hdl = face.expressInterest(*makeInterest("/Hello/World", true, 50_ms),
-                                  std::bind([] { BOOST_FAIL("Unexpected data"); }),
-                                  std::bind([] { BOOST_FAIL("Unexpected nack"); }),
-                                  std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                                  [] (auto&&...) { BOOST_FAIL("Unexpected data"); },
+                                  [] (auto&&...) { BOOST_FAIL("Unexpected nack"); },
+                                  [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
   advanceClocks(1_ms);
   hdl.cancel();
   advanceClocks(1_ms);
@@ -302,9 +300,9 @@
   // cancel after destructing face
   auto face2 = make_unique<DummyClientFace>(m_io, m_keyChain);
   auto hdl2 = face2->expressInterest(*makeInterest("/Hello/World", true, 50_ms),
-                                     std::bind([] { BOOST_FAIL("Unexpected data"); }),
-                                     std::bind([] { BOOST_FAIL("Unexpected nack"); }),
-                                     std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                                     [] (auto&&...) { BOOST_FAIL("Unexpected data"); },
+                                     [] (auto&&...) { BOOST_FAIL("Unexpected nack"); },
+                                     [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
   advanceClocks(1_ms);
   face2.reset();
   advanceClocks(1_ms);
@@ -320,14 +318,14 @@
 BOOST_AUTO_TEST_CASE(RemoveAllPendingInterests)
 {
   face.expressInterest(*makeInterest("/Hello/World/0", false, 50_ms),
-                       std::bind([] { BOOST_FAIL("Unexpected data"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected data"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   face.expressInterest(*makeInterest("/Hello/World/1", false, 50_ms),
-                       std::bind([] { BOOST_FAIL("Unexpected data"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected data"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
 
   advanceClocks(10_ms);
 
@@ -370,18 +368,19 @@
   bool hasInterest1 = false, hasData = false;
 
   // first InterestFilter allows loopback and should receive Interest
-  face.setInterestFilter("/", [&] (const InterestFilter&, const Interest&) {
+  face.setInterestFilter("/", [&] (auto&&...) {
     hasInterest1 = true;
     // do not respond with Data right away, so Face must send Interest to forwarder
   });
+
   // second InterestFilter disallows loopback and should not receive Interest
   face.setInterestFilter(InterestFilter("/").allowLoopback(false),
-    std::bind([] { BOOST_ERROR("Unexpected Interest on second InterestFilter"); }));
+                         [] (auto&&...) { BOOST_ERROR("Unexpected Interest on second InterestFilter"); });
 
   face.expressInterest(*makeInterest("/A", true),
-                       std::bind([&] { hasData = true; }),
-                       std::bind([] { BOOST_FAIL("Unexpected nack"); }),
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [&] (auto&&...) { hasData = true; },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected nack"); },
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
   advanceClocks(1_ms);
   BOOST_CHECK_EQUAL(hasInterest1, true); // Interest looped back
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 1); // Interest sent to forwarder
@@ -397,12 +396,12 @@
 {
   bool hasInterest1 = false;
   // register two Interest destinations
-  face.setInterestFilter("/", std::bind([&] {
+  face.setInterestFilter("/", [&] (auto&&...) {
     hasInterest1 = true;
     // sending Data right away from the first destination, don't care whether Interest goes to second destination
     face.put(*makeData("/A/B"));
-  }));
-  face.setInterestFilter("/", std::bind([]{}));
+  });
+  face.setInterestFilter("/", [] (auto&&...) {});
   advanceClocks(10_ms);
 
   face.receive(*makeInterest("/A", true));
@@ -417,7 +416,8 @@
 
 BOOST_AUTO_TEST_CASE(PutNack)
 {
-  face.setInterestFilter("/", std::bind([]{})); // register one Interest destination so that face can accept Nacks
+  // register one Interest destination so that face can accept Nacks
+  face.setInterestFilter("/", [] (auto&&...) {});
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(face.sentNacks.size(), 0);
@@ -457,7 +457,7 @@
     // sending Nack right away from the first destination, Interest should still go to second destination
     face.put(makeNack(interest, lp::NackReason::CONGESTION));
   });
-  face.setInterestFilter("/", std::bind([&] { hasInterest2 = true; }));
+  face.setInterestFilter("/", [&] (auto&&...) { hasInterest2 = true; });
   advanceClocks(10_ms);
 
   auto interest = makeInterest("/A", false, std::nullopt, 14333271);
@@ -487,18 +487,19 @@
     hasInterest1 = true;
     face.put(makeNack(interest, lp::NackReason::CONGESTION));
   });
+
   // second InterestFilter disallows loopback and should not receive Interest
   face.setInterestFilter(InterestFilter("/").allowLoopback(false),
-    std::bind([] { BOOST_ERROR("Unexpected Interest on second InterestFilter"); }));
+                         [] (auto&&...) { BOOST_ERROR("Unexpected Interest on second InterestFilter"); });
 
   auto interest = makeInterest("/A", false, std::nullopt, 28395852);
   face.expressInterest(*interest,
-                       std::bind([] { BOOST_FAIL("Unexpected data"); }),
+                       [] (auto&&...) { BOOST_FAIL("Unexpected data"); },
                        [&] (const Interest&, const lp::Nack& nack) {
                          hasNack = true;
                          BOOST_CHECK_EQUAL(nack.getReason(), lp::NackReason::CONGESTION);
                        },
-                       std::bind([] { BOOST_FAIL("Unexpected timeout"); }));
+                       [] (auto&&...) { BOOST_FAIL("Unexpected timeout"); });
   advanceClocks(1_ms);
   BOOST_CHECK_EQUAL(hasInterest1, true); // Interest looped back
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 1); // Interest sent to forwarder
@@ -553,7 +554,7 @@
   // cancel after destructing face
   auto face2 = make_unique<DummyClientFace>(m_io, m_keyChain);
   hdl = face2->registerPrefix("/Hello/World/2", nullptr,
-                              std::bind([] { BOOST_FAIL("Unexpected registerPrefix failure"); }));
+                              [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
   advanceClocks(1_ms);
   face2.reset();
   advanceClocks(1_ms);
@@ -563,7 +564,7 @@
   // unregister after destructing face
   auto face3 = make_unique<DummyClientFace>(m_io, m_keyChain);
   hdl = face3->registerPrefix("/Hello/World/3", nullptr,
-                              std::bind([] { BOOST_FAIL("Unexpected registerPrefix failure"); }));
+                              [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
   advanceClocks(1_ms);
   face3.reset();
   advanceClocks(1_ms);
@@ -579,9 +580,9 @@
   size_t nInterests = 0;
   size_t nRegs = 0;
   auto hdl = face.setInterestFilter("/Hello/World",
-                                    std::bind([&nInterests] { ++nInterests; }),
-                                    std::bind([&nRegs] { ++nRegs; }),
-                                    std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                                    [&] (auto&&...) { ++nInterests; },
+                                    [&] (auto&&...) { ++nRegs; },
+                                    [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
   advanceClocks(25_ms, 4);
   BOOST_CHECK_EQUAL(nRegs, 1);
   BOOST_CHECK_EQUAL(nInterests, 0);
@@ -623,8 +624,8 @@
 {
   size_t nInterests = 0;
   auto hdl = face.setInterestFilter("/Hello/World",
-                                    std::bind([&nInterests] { ++nInterests; }),
-                                    std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                                    [&] (auto&&...) { ++nInterests; },
+                                    [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
   advanceClocks(25_ms, 4);
   BOOST_CHECK_EQUAL(nInterests, 0);
 
@@ -654,9 +655,9 @@
   // don't enable registration reply
   size_t nRegFailed = 0;
   face.setInterestFilter("/Hello/World",
-                         std::bind([] { BOOST_FAIL("Unexpected Interest"); }),
-                         std::bind([] { BOOST_FAIL("Unexpected success of setInterestFilter"); }),
-                         std::bind([&nRegFailed] { ++nRegFailed; }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected Interest"); },
+                         [] (auto&&...) { BOOST_FAIL("Unexpected success"); },
+                         [&] (auto&&...) { ++nRegFailed; });
 
   advanceClocks(25_ms, 4);
   BOOST_CHECK_EQUAL(nRegFailed, 0);
@@ -670,8 +671,8 @@
   // don't enable registration reply
   size_t nRegFailed = 0;
   face.setInterestFilter("/Hello/World",
-                         std::bind([] { BOOST_FAIL("Unexpected Interest"); }),
-                         std::bind([&nRegFailed] { ++nRegFailed; }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected Interest"); },
+                         [&] (auto&&...) { ++nRegFailed; });
 
   advanceClocks(25_ms, 4);
   BOOST_CHECK_EQUAL(nRegFailed, 0);
@@ -684,21 +685,21 @@
 {
   size_t nInInterests1 = 0;
   face.setInterestFilter("/Hello/World",
-                         std::bind([&nInInterests1] { ++nInInterests1; }),
+                         [&nInInterests1] (auto&&...) { ++nInInterests1; },
                          nullptr,
-                         std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
   size_t nInInterests2 = 0;
   face.setInterestFilter("/Hello",
-                         std::bind([&nInInterests2] { ++nInInterests2; }),
+                         [&nInInterests2] (auto&&...) { ++nInInterests2; },
                          nullptr,
-                         std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
   size_t nInInterests3 = 0;
   face.setInterestFilter("/Los/Angeles/Lakers",
-                         std::bind([&nInInterests3] { ++nInInterests3; }),
+                         [&nInInterests3] (auto&&...) { ++nInInterests3; },
                          nullptr,
-                         std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
   advanceClocks(25_ms, 4);
 
@@ -714,9 +715,9 @@
 {
   size_t nInInterests = 0;
   face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
-                         std::bind([&nInInterests] { ++nInInterests; }),
+                         [&nInInterests] (auto&&...) { ++nInInterests; },
                          nullptr,
-                         std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
   advanceClocks(25_ms, 4);
 
@@ -736,11 +737,14 @@
 BOOST_AUTO_TEST_CASE(RegexFilterError)
 {
   face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
+                         // Do NOT use 'auto' for this lambda. This is testing the (failure of)
+                         // implicit conversion from InterestFilter to Name, therefore the type
+                         // of the first parameter must be explicit.
                          [] (const Name&, const Interest&) {
-                           BOOST_FAIL("InterestFilter::Error should have been triggered");
+                           BOOST_FAIL("InterestFilter::Error should have been raised");
                          },
                          nullptr,
-                         std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                         [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
   advanceClocks(25_ms, 4);
 
@@ -751,12 +755,12 @@
 {
   size_t nInInterests = 0;
   face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
-                         std::bind([&nInInterests] { ++nInInterests; }));
+                         [&] (auto&&...) { ++nInInterests; });
 
   size_t nRegSuccesses = 0;
   face.registerPrefix("/Hello/World",
-                      std::bind([&nRegSuccesses] { ++nRegSuccesses; }),
-                      std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                      [&] (auto&&...) { ++nRegSuccesses; },
+                      [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
   advanceClocks(25_ms, 4);
   BOOST_CHECK_EQUAL(nRegSuccesses, 1);
@@ -780,11 +784,11 @@
   // Regular Face won't accept incoming packets until something is sent.
 
   int hit = 0;
-  face.setInterestFilter(Name("/"), std::bind([&hit] { ++hit; }));
-  face.processEvents(time::milliseconds(-1));
+  face.setInterestFilter(Name("/"), [&hit] (auto&&...) { ++hit; });
+  face.processEvents(-1_ms);
 
   face.receive(*makeInterest("/A"));
-  face.processEvents(time::milliseconds(-1));
+  face.processEvents(-1_ms);
 
   BOOST_CHECK_EQUAL(hit, 1);
 }
@@ -792,7 +796,7 @@
 BOOST_AUTO_TEST_CASE(Handle)
 {
   int hit = 0;
-  InterestFilterHandle hdl = face.setInterestFilter(Name("/"), std::bind([&hit] { ++hit; }));
+  InterestFilterHandle hdl = face.setInterestFilter(Name("/"), [&hit] (auto&&...) { ++hit; });
   face.processEvents(-1_ms);
 
   face.receive(*makeInterest("/A"));
@@ -820,18 +824,18 @@
 
 BOOST_AUTO_TEST_CASE(ProcessEvents)
 {
-  face.processEvents(time::milliseconds(-1)); // io_service::reset()/poll() inside
+  face.processEvents(-1_ms); // io_context::restart()/poll() inside
 
-  size_t nRegSuccesses = 0;
+  int nRegSuccesses = 0;
   face.registerPrefix("/Hello/World",
-                      std::bind([&nRegSuccesses] { ++nRegSuccesses; }),
-                      std::bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+                      [&] (auto&&...) { ++nRegSuccesses; },
+                      [] (auto&&...) { BOOST_FAIL("Unexpected failure"); });
 
-  // io_service::poll() without reset
-  face.getIoService().poll();
+  // io_context::poll() without reset
+  face.getIoContext().poll();
   BOOST_CHECK_EQUAL(nRegSuccesses, 0);
 
-  face.processEvents(time::milliseconds(-1)); // io_service::reset()/poll() inside
+  face.processEvents(-1_ms); // io_context::restart()/poll() inside
   BOOST_CHECK_EQUAL(nRegSuccesses, 1);
 }
 
@@ -852,16 +856,14 @@
 
 BOOST_FIXTURE_TEST_CASE(FaceTransport, IoKeyChainFixture)
 {
-  BOOST_CHECK(Face().getTransport() != nullptr);
-
-  BOOST_CHECK(Face(shared_ptr<Transport>()).getTransport() != nullptr);
-  BOOST_CHECK(Face(shared_ptr<Transport>(), m_io).getTransport() != nullptr);
-  BOOST_CHECK(Face(shared_ptr<Transport>(), m_io, m_keyChain).getTransport() != nullptr);
+  BOOST_CHECK_NO_THROW(Face(shared_ptr<Transport>()));
+  BOOST_CHECK_NO_THROW(Face(shared_ptr<Transport>(), m_io));
+  BOOST_CHECK_NO_THROW(Face(shared_ptr<Transport>(), m_io, m_keyChain));
 
   auto transport = make_shared<TcpTransport>("localhost", "6363"); // no real io operations will be scheduled
-  BOOST_CHECK(Face(transport).getTransport() == transport);
-  BOOST_CHECK(Face(transport, m_io).getTransport() == transport);
-  BOOST_CHECK(Face(transport, m_io, m_keyChain).getTransport() == transport);
+  BOOST_CHECK(&Face(transport).getTransport() == transport.get());
+  BOOST_CHECK(&Face(transport, m_io).getTransport() == transport.get());
+  BOOST_CHECK(&Face(transport, m_io, m_keyChain).getTransport() == transport.get());
 }
 
 class WithEnv
@@ -914,8 +916,8 @@
 BOOST_FIXTURE_TEST_CASE(NoConfig, WithEnvAndConfig) // fixture configures test HOME and PIB/TPM path
 {
   shared_ptr<Face> face;
-  BOOST_REQUIRE_NO_THROW(face = make_shared<Face>());
-  BOOST_CHECK(dynamic_pointer_cast<UnixTransport>(face->getTransport()) != nullptr);
+  BOOST_CHECK_NO_THROW(face = make_shared<Face>());
+  BOOST_CHECK(dynamic_cast<UnixTransport*>(&face->getTransport()) != nullptr);
 }
 
 BOOST_FIXTURE_TEST_CASE_TEMPLATE(Unix, T, ConfigOptions, T)
@@ -923,8 +925,8 @@
   this->configure("unix://some/path");
 
   shared_ptr<Face> face;
-  BOOST_REQUIRE_NO_THROW(face = make_shared<Face>());
-  BOOST_CHECK(dynamic_pointer_cast<UnixTransport>(face->getTransport()) != nullptr);
+  BOOST_CHECK_NO_THROW(face = make_shared<Face>());
+  BOOST_CHECK(dynamic_cast<UnixTransport*>(&face->getTransport()) != nullptr);
 }
 
 BOOST_FIXTURE_TEST_CASE_TEMPLATE(Tcp, T, ConfigOptions, T)
@@ -932,8 +934,8 @@
   this->configure("tcp://127.0.0.1:6000");
 
   shared_ptr<Face> face;
-  BOOST_REQUIRE_NO_THROW(face = make_shared<Face>());
-  BOOST_CHECK(dynamic_pointer_cast<TcpTransport>(face->getTransport()) != nullptr);
+  BOOST_CHECK_NO_THROW(face = make_shared<Face>());
+  BOOST_CHECK(dynamic_cast<TcpTransport*>(&face->getTransport()) != nullptr);
 }
 
 BOOST_FIXTURE_TEST_CASE_TEMPLATE(WrongTransport, T, ConfigOptions, T)
@@ -956,8 +958,8 @@
   this->WithConfig::configure("unix://some/path");
 
   shared_ptr<Face> face;
-  BOOST_REQUIRE_NO_THROW(face = make_shared<Face>());
-  BOOST_CHECK(dynamic_pointer_cast<TcpTransport>(face->getTransport()) != nullptr);
+  BOOST_CHECK_NO_THROW(face = make_shared<Face>());
+  BOOST_CHECK(dynamic_cast<TcpTransport*>(&face->getTransport()) != nullptr);
 }
 
 BOOST_FIXTURE_TEST_CASE(ExplicitTransport, WithEnvAndConfig)
@@ -967,8 +969,8 @@
 
   auto transport = make_shared<UnixTransport>("unix://some/path");
   shared_ptr<Face> face;
-  BOOST_REQUIRE_NO_THROW(face = make_shared<Face>(transport));
-  BOOST_CHECK(dynamic_pointer_cast<UnixTransport>(face->getTransport()) != nullptr);
+  BOOST_CHECK_NO_THROW(face = make_shared<Face>(transport));
+  BOOST_CHECK(dynamic_cast<UnixTransport*>(&face->getTransport()) != nullptr);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // Transport
diff --git a/tests/unit/io-fixture.hpp b/tests/unit/io-fixture.hpp
index 191b889..762dd6c 100644
--- a/tests/unit/io-fixture.hpp
+++ b/tests/unit/io-fixture.hpp
@@ -24,7 +24,7 @@
 
 #include "tests/unit/clock-fixture.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 
 namespace ndn::tests {
 
@@ -41,7 +41,7 @@
   }
 
 protected:
-  boost::asio::io_service m_io;
+  boost::asio::io_context m_io;
 };
 
 } // namespace ndn::tests
diff --git a/tests/unit/net/dns.t.cpp b/tests/unit/net/dns.t.cpp
index ac734dd..17c5438 100644
--- a/tests/unit/net/dns.t.cpp
+++ b/tests/unit/net/dns.t.cpp
@@ -24,20 +24,19 @@
 #include "tests/boost-test.hpp"
 #include "tests/unit/net/network-configuration-detector.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 
 namespace ndn::tests {
 
 using namespace ndn::dns;
-using boost::asio::ip::address_v4;
-using boost::asio::ip::address_v6;
+namespace ip = boost::asio::ip;
 
 class DnsFixture
 {
 public:
   void
-  onSuccess(const IpAddress& resolvedAddress,
-            const IpAddress& expectedAddress,
+  onSuccess(const boost::asio::ip::address& resolvedAddress,
+            const boost::asio::ip::address& expectedAddress,
             bool isValid,
             bool shouldCheckAddress = false)
   {
@@ -66,105 +65,95 @@
 protected:
   int m_nFailures = 0;
   int m_nSuccesses = 0;
-  boost::asio::io_service m_ioService;
+  boost::asio::io_context m_ioCtx;
 };
 
 BOOST_AUTO_TEST_SUITE(Net)
 BOOST_FIXTURE_TEST_SUITE(TestDns, DnsFixture)
 
-BOOST_AUTO_TEST_CASE(Asynchronous)
+BOOST_AUTO_TEST_CASE(Failure)
 {
   SKIP_IF_IP_UNAVAILABLE();
 
   asyncResolve("nothost.nothost.nothost.arpa",
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v4()), false, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v4(), false, false),
                [this] (auto&&...) { onFailure(true); },
-               m_ioService); // should fail
+               m_ioCtx); // should fail
 
-  m_ioService.run();
+  m_ioCtx.run();
   BOOST_CHECK_EQUAL(m_nFailures, 1);
   BOOST_CHECK_EQUAL(m_nSuccesses, 0);
 }
 
-BOOST_AUTO_TEST_CASE(AsynchronousV4)
+BOOST_AUTO_TEST_CASE(Ipv4)
 {
   SKIP_IF_IPV4_UNAVAILABLE();
 
   asyncResolve("192.0.2.1",
-               std::bind(&DnsFixture::onSuccess, this, _1,
-                         IpAddress(address_v4::from_string("192.0.2.1")), true, true),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::make_address_v4("192.0.2.1"), true, true),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService);
+               m_ioCtx);
 
-  m_ioService.run();
+  m_ioCtx.run();
   BOOST_CHECK_EQUAL(m_nFailures, 0);
   BOOST_CHECK_EQUAL(m_nSuccesses, 1);
 }
 
-BOOST_AUTO_TEST_CASE(AsynchronousV6)
+BOOST_AUTO_TEST_CASE(Ipv6)
 {
   SKIP_IF_IPV6_UNAVAILABLE();
 
   asyncResolve("ipv6.google.com", // only IPv6 address should be available
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), true, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v6(), true, false),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService);
+               m_ioCtx);
 
   asyncResolve("2001:db8:3f9:0:3025:ccc5:eeeb:86d3",
                std::bind(&DnsFixture::onSuccess, this, _1,
-                         IpAddress(address_v6::from_string("2001:db8:3f9:0:3025:ccc5:eeeb:86d3")),
-                         true, true),
+                         ip::make_address_v6("2001:db8:3f9:0:3025:ccc5:eeeb:86d3"), true, true),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService);
+               m_ioCtx);
 
-  m_ioService.run();
+  m_ioCtx.run();
   BOOST_CHECK_EQUAL(m_nFailures, 0);
   BOOST_CHECK_EQUAL(m_nSuccesses, 2);
 }
 
-BOOST_AUTO_TEST_CASE(AsynchronousV4AndV6)
+BOOST_AUTO_TEST_CASE(WithAddressSelector)
 {
   SKIP_IF_IPV4_UNAVAILABLE();
   SKIP_IF_IPV6_UNAVAILABLE();
 
   asyncResolve("named-data.net",
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v4()), true, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v4(), true, false),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService, Ipv4Only());
+               m_ioCtx, Ipv4Only());
 
   asyncResolve("a.root-servers.net",
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v4()), true, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v4(), true, false),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService, Ipv4Only()); // request IPv4 address
+               m_ioCtx, Ipv4Only()); // request IPv4 address
 
   asyncResolve("a.root-servers.net",
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), true, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v6(), true, false),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService, Ipv6Only()); // request IPv6 address
+               m_ioCtx, Ipv6Only()); // request IPv6 address
 
   asyncResolve("ipv6.google.com", // only IPv6 address should be available
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), true, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v6(), true, false),
                [this] (auto&&...) { onFailure(false); },
-               m_ioService, Ipv6Only());
+               m_ioCtx, Ipv6Only());
 
   asyncResolve("ipv6.google.com", // only IPv6 address should be available
-               std::bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), false, false),
+               std::bind(&DnsFixture::onSuccess, this, _1, ip::address_v6(), false, false),
                [this] (auto&&...) { onFailure(true); },
-               m_ioService, Ipv4Only()); // should fail
+               m_ioCtx, Ipv4Only()); // should fail
 
-  m_ioService.run();
+  m_ioCtx.run();
   BOOST_CHECK_EQUAL(m_nFailures, 1);
   BOOST_CHECK_EQUAL(m_nSuccesses, 4);
 }
 
-BOOST_AUTO_TEST_CASE(Synchronous)
-{
-  SKIP_IF_IP_UNAVAILABLE();
-
-  IpAddress address = syncResolve("named-data.net", m_ioService);
-  BOOST_CHECK(address.is_v4() || address.is_v6());
-}
-
 BOOST_AUTO_TEST_SUITE_END() // TestDns
 BOOST_AUTO_TEST_SUITE_END() // Net
 
diff --git a/tests/unit/net/face-uri.t.cpp b/tests/unit/net/face-uri.t.cpp
index 78e712a..36883bf 100644
--- a/tests/unit/net/face-uri.t.cpp
+++ b/tests/unit/net/face-uri.t.cpp
@@ -94,7 +94,7 @@
   shared_ptr<const net::NetworkInterface> m_netif;
 
 private:
-  boost::asio::io_service m_io;
+  boost::asio::io_context m_io;
 };
 
 BOOST_AUTO_TEST_CASE(ParseInternal)
@@ -147,11 +147,11 @@
 
   namespace ip = boost::asio::ip;
 
-  ip::udp::endpoint endpoint4(ip::address_v4::from_string("192.0.2.1"), 7777);
+  ip::udp::endpoint endpoint4(ip::make_address_v4("192.0.2.1"), 7777);
   uri = FaceUri(endpoint4);
   BOOST_CHECK_EQUAL(uri.toString(), "udp4://192.0.2.1:7777");
 
-  ip::udp::endpoint endpoint6(ip::address_v6::from_string("2001:DB8::1"), 7777);
+  ip::udp::endpoint endpoint6(ip::make_address_v6("2001:DB8::1"), 7777);
   uri = FaceUri(endpoint6);
   BOOST_CHECK_EQUAL(uri.toString(), "udp6://[2001:db8::1]:7777");
 
@@ -285,14 +285,14 @@
 
   namespace ip = boost::asio::ip;
 
-  ip::tcp::endpoint endpoint4(ip::address_v4::from_string("192.0.2.1"), 7777);
+  ip::tcp::endpoint endpoint4(ip::make_address_v4("192.0.2.1"), 7777);
   uri = FaceUri(endpoint4);
   BOOST_CHECK_EQUAL(uri.toString(), "tcp4://192.0.2.1:7777");
 
   uri = FaceUri(endpoint4, "wsclient");
   BOOST_CHECK_EQUAL(uri.toString(), "wsclient://192.0.2.1:7777");
 
-  ip::tcp::endpoint endpoint6(ip::address_v6::from_string("2001:DB8::1"), 7777);
+  ip::tcp::endpoint endpoint6(ip::make_address_v6("2001:DB8::1"), 7777);
   uri = FaceUri(endpoint6);
   BOOST_CHECK_EQUAL(uri.toString(), "tcp6://[2001:db8::1]:7777");
 
@@ -553,7 +553,7 @@
 
 BOOST_AUTO_TEST_CASE(CanonizeEmptyCallback)
 {
-  boost::asio::io_service io;
+  boost::asio::io_context io;
 
   // unsupported scheme
   FaceUri("null://").canonize(nullptr, nullptr, io, 1_ms);
diff --git a/tests/unit/net/network-configuration-detector.cpp b/tests/unit/net/network-configuration-detector.cpp
index f8a8816..44cae39 100644
--- a/tests/unit/net/network-configuration-detector.cpp
+++ b/tests/unit/net/network-configuration-detector.cpp
@@ -21,11 +21,9 @@
 
 #include "tests/unit/net/network-configuration-detector.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
 #include <boost/asio/ip/address.hpp>
-#include <boost/asio/ip/basic_resolver.hpp>
 #include <boost/asio/ip/udp.hpp>
-#include <boost/range/iterator_range_core.hpp>
 
 namespace ndn::tests {
 
@@ -50,31 +48,23 @@
 void
 NetworkConfigurationDetector::detect()
 {
-  using BoostResolver = boost::asio::ip::basic_resolver<boost::asio::ip::udp>;
+  boost::asio::io_context io;
+  boost::asio::ip::udp::resolver resolver(io);
 
-  boost::asio::io_service ioService;
-  BoostResolver resolver(ioService);
+  boost::system::error_code ec;
+  // The specified hostname must have both A and AAAA records
+  auto results = resolver.resolve("a.root-servers.net", "", ec);
 
-  // The specified hostname must contain both A and AAAA records
-  BoostResolver::query query("a.root-servers.net", "");
-
-  boost::system::error_code errorCode;
-  BoostResolver::iterator begin = resolver.resolve(query, errorCode);
-  if (errorCode) {
-    s_isInitialized = true;
-    return;
-  }
-  BoostResolver::iterator end;
-
-  for (const auto& i : boost::make_iterator_range(begin, end)) {
-    if (i.endpoint().address().is_v4()) {
-      s_hasIpv4 = true;
-    }
-    else if (i.endpoint().address().is_v6()) {
-      s_hasIpv6 = true;
+  if (!ec) {
+    for (const auto& i : results) {
+      if (i.endpoint().address().is_v4()) {
+        s_hasIpv4 = true;
+      }
+      else if (i.endpoint().address().is_v6()) {
+        s_hasIpv6 = true;
+      }
     }
   }
-
   s_isInitialized = true;
 }
 
diff --git a/tests/unit/net/network-monitor.t.cpp b/tests/unit/net/network-monitor.t.cpp
index 4515258..50c04c7 100644
--- a/tests/unit/net/network-monitor.t.cpp
+++ b/tests/unit/net/network-monitor.t.cpp
@@ -23,7 +23,8 @@
 
 #include "tests/boost-test.hpp"
 
-#include <boost/asio/io_service.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
 
 namespace ndn::tests {
 
@@ -42,7 +43,7 @@
 
 BOOST_AUTO_TEST_CASE(DestructWithoutRun)
 {
-  boost::asio::io_service io;
+  boost::asio::io_context io;
   auto nm = make_unique<NetworkMonitor>(io);
   nm.reset();
   BOOST_CHECK(true); // if we got this far, the test passed
@@ -50,16 +51,16 @@
 
 BOOST_AUTO_TEST_CASE(DestructWhileEnumerating)
 {
-  boost::asio::io_service io;
+  boost::asio::io_context io;
   auto nm = make_unique<NetworkMonitor>(io);
   NM_REQUIRE_CAP(ENUM);
 
   nm->onInterfaceAdded.connect([&] (const shared_ptr<const NetworkInterface>&) {
-    io.post([&] { nm.reset(); });
+    boost::asio::post(io, [&] { nm.reset(); });
   });
   nm->onEnumerationCompleted.connect([&] {
     // make sure the test case terminates even if we have zero interfaces
-    io.post([&] { nm.reset(); });
+    boost::asio::post(io, [&] { nm.reset(); });
   });
 
   io.run();
diff --git a/tests/unit/security/validator-fixture.cpp b/tests/unit/security/validator-fixture.cpp
index 41497bf..ff198a3 100644
--- a/tests/unit/security/validator-fixture.cpp
+++ b/tests/unit/security/validator-fixture.cpp
@@ -24,6 +24,8 @@
 #include "ndn-cxx/security/additional-description.hpp"
 #include "ndn-cxx/util/signal/scoped-connection.hpp"
 
+#include <boost/asio/post.hpp>
+
 namespace ndn::tests {
 
 using namespace ndn::security;
@@ -47,7 +49,7 @@
 {
   signal::ScopedConnection conn = face.onSendInterest.connect([this] (const Interest& interest) {
     if (processInterest) {
-      m_io.post([=] { processInterest(interest); });
+      boost::asio::post(m_io, [=] { processInterest(interest); });
     }
   });
   advanceClocks(s_mockPeriod, s_mockTimes);
@@ -69,8 +71,7 @@
                .appendVersion());
 
   SignatureInfo info;
-  auto now = time::system_clock::now();
-  info.setValidityPeriod(ValidityPeriod(now, now + 90_days));
+  info.setValidityPeriod(ValidityPeriod::makeRelative(0_s, 90_days));
 
   AdditionalDescription description;
   description.set("type", "sub-certificate");
diff --git a/tests/unit/security/validator-null.t.cpp b/tests/unit/security/validator-null.t.cpp
index ebae900..05dd270 100644
--- a/tests/unit/security/validator-null.t.cpp
+++ b/tests/unit/security/validator-null.t.cpp
@@ -39,8 +39,8 @@
 
   ValidatorNull validator;
   validator.validate(data,
-                     std::bind([] { BOOST_CHECK_MESSAGE(true, "Validation should succeed"); }),
-                     std::bind([] { BOOST_CHECK_MESSAGE(false, "Validation should not have failed"); }));
+                     [] (auto&&...) { BOOST_CHECK_MESSAGE(true, "Validation should succeed"); },
+                     [] (auto&&...) { BOOST_CHECK_MESSAGE(false, "Validation should not have failed"); });
 }
 
 BOOST_AUTO_TEST_CASE(ValidateInterest)
@@ -51,8 +51,8 @@
 
   ValidatorNull validator;
   validator.validate(interest,
-                     std::bind([] { BOOST_CHECK_MESSAGE(true, "Validation should succeed"); }),
-                     std::bind([] { BOOST_CHECK_MESSAGE(false, "Validation should not have failed"); }));
+                     [] (auto&&...) { BOOST_CHECK_MESSAGE(true, "Validation should succeed"); },
+                     [] (auto&&...) { BOOST_CHECK_MESSAGE(false, "Validation should not have failed"); });
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestValidatorNull
diff --git a/tests/unit/util/regex.t.cpp b/tests/unit/util/regex.t.cpp
index d09688e..07ec293 100644
--- a/tests/unit/util/regex.t.cpp
+++ b/tests/unit/util/regex.t.cpp
@@ -279,7 +279,7 @@
 {
   auto backRef = make_shared<RegexBackrefManager>();
   auto cm = make_shared<RegexBackrefMatcher>("(<a><b>)", backRef);
-  backRef->pushRef(static_pointer_cast<RegexMatcher>(cm));
+  backRef->pushRef(std::static_pointer_cast<RegexMatcher>(cm));
   cm->compile();
   bool res = cm->match(Name("/a/b/c"), 0, 2);
   BOOST_CHECK_EQUAL(res, true);
diff --git a/tests/unit/util/scheduler.t.cpp b/tests/unit/util/scheduler.t.cpp
index e5c99e5..88c4313 100644
--- a/tests/unit/util/scheduler.t.cpp
+++ b/tests/unit/util/scheduler.t.cpp
@@ -51,23 +51,31 @@
     BOOST_CHECK_EQUAL(count2, 1);
   });
 
-  EventId i = scheduler.schedule(1_s, [] { BOOST_ERROR("This event should not have been fired"); });
-  i.cancel();
+  EventId eid = scheduler.schedule(1_s, [] { BOOST_ERROR("This event should not have been fired"); });
+  eid.cancel();
 
   scheduler.schedule(250_ms, [&] {
     BOOST_CHECK_EQUAL(count1, 0);
     ++count2;
   });
 
-  i = scheduler.schedule(50_ms, [&] { BOOST_ERROR("This event should not have been fired"); });
-  i.cancel();
+  eid = scheduler.schedule(50_ms, [&] { BOOST_ERROR("This event should not have been fired"); });
+  eid.cancel();
 
   advanceClocks(25_ms, 1000_ms);
   BOOST_CHECK_EQUAL(count1, 1);
   BOOST_CHECK_EQUAL(count2, 1);
 }
 
-BOOST_AUTO_TEST_CASE(CallbackException)
+BOOST_AUTO_TEST_CASE(NegativeDelay)
+{
+  bool wasCallbackInvoked = false;
+  scheduler.schedule(-1_s, [&] { wasCallbackInvoked = true; });
+  advanceClocks(1_ns);
+  BOOST_CHECK(wasCallbackInvoked);
+}
+
+BOOST_AUTO_TEST_CASE(ThrowingCallback)
 {
   class MyException : public std::exception
   {
@@ -79,12 +87,12 @@
     throw MyException{};
   });
 
-  bool isCallbackInvoked = false;
-  scheduler.schedule(20_ms, [&isCallbackInvoked] { isCallbackInvoked = true; });
+  bool wasCallbackInvoked = false;
+  scheduler.schedule(20_ms, [&] { wasCallbackInvoked = true; });
 
   BOOST_CHECK_THROW(this->advanceClocks(6_ms, 2), MyException);
   this->advanceClocks(6_ms, 2);
-  BOOST_CHECK(isCallbackInvoked);
+  BOOST_CHECK(wasCallbackInvoked);
 }
 
 BOOST_AUTO_TEST_CASE(CancelEmptyEvent)
@@ -100,7 +108,7 @@
 {
   EventId selfEventId;
   selfEventId = scheduler.schedule(100_ms, [&] { selfEventId.cancel(); });
-  BOOST_REQUIRE_NO_THROW(advanceClocks(100_ms, 10));
+  BOOST_CHECK_NO_THROW(advanceClocks(100_ms, 10));
 }
 
 class SelfRescheduleFixture : public SchedulerFixture
@@ -143,7 +151,7 @@
     scheduler.schedule(100_ms, [&] { ++count; });
   }
 
-public:
+protected:
   EventId selfEventId;
   size_t count = 0;
 };
@@ -151,21 +159,21 @@
 BOOST_FIXTURE_TEST_CASE(Reschedule, SelfRescheduleFixture)
 {
   selfEventId = scheduler.schedule(0_s, [this] { reschedule(); });
-  BOOST_REQUIRE_NO_THROW(advanceClocks(50_ms, 1000_ms));
+  advanceClocks(50_ms, 1000_ms);
   BOOST_CHECK_EQUAL(count, 5);
 }
 
 BOOST_FIXTURE_TEST_CASE(Reschedule2, SelfRescheduleFixture)
 {
   selfEventId = scheduler.schedule(0_s, [this] { reschedule2(); });
-  BOOST_REQUIRE_NO_THROW(advanceClocks(50_ms, 1000_ms));
+  advanceClocks(50_ms, 1000_ms);
   BOOST_CHECK_EQUAL(count, 5);
 }
 
 BOOST_FIXTURE_TEST_CASE(Reschedule3, SelfRescheduleFixture)
 {
   selfEventId = scheduler.schedule(0_s, [this] { reschedule3(); });
-  BOOST_REQUIRE_NO_THROW(advanceClocks(50_ms, 1000_ms));
+  advanceClocks(50_ms, 1000_ms);
   BOOST_CHECK_EQUAL(count, 6);
 }
 
diff --git a/wscript b/wscript
index 5eb2681..a55e483 100644
--- a/wscript
+++ b/wscript
@@ -140,8 +140,6 @@
     if conf.env.WITH_TOOLS:
         conf.check_boost(lib='program_options', mt=True, uselib_store='BOOST_TOOLS')
 
-    conf.env.append_unique('DEFINES_BOOST', ['BOOST_FILESYSTEM_NO_DEPRECATED'])
-
     conf.check_compiler_flags()
 
     # Loading "late" to prevent tests from being compiled with profiling flags