diff --git a/typo3/sysext/backend/Configuration/Commands.php b/typo3/sysext/backend/Configuration/Commands.php
deleted file mode 100644
index 7a7a9420e93b1447c131c2b694bf753c4d052170..0000000000000000000000000000000000000000
--- a/typo3/sysext/backend/Configuration/Commands.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-/**
- * Commands to be executed by typo3, where the key of the array
- * is the name of the command (to be called as the first argument after typo3).
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- *
- * example: bin/typo3 backend:lock
- */
-return [
-    'backend:lock' => [
-        'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class
-    ],
-    'backend:unlock' => [
-        'class' => \TYPO3\CMS\Backend\Command\UnlockBackendCommand::class
-    ],
-    'referenceindex:update' => [
-        'class' => \TYPO3\CMS\Backend\Command\ReferenceIndexUpdateCommand::class
-    ]
-];
diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml
index 3dd9134617582151ac8c1cf65946d3159da61fe1..14d7049a180516d70a3beafa2e400dbe7f82cf18 100644
--- a/typo3/sysext/backend/Configuration/Services.yaml
+++ b/typo3/sysext/backend/Configuration/Services.yaml
@@ -7,6 +7,18 @@ services:
   TYPO3\CMS\Backend\:
     resource: '../Classes/*'
 
+  TYPO3\CMS\Backend\Command\LockBackendCommand:
+    tags:
+      - { name: 'console.command', command: 'backend:lock' }
+
+  TYPO3\CMS\Backend\Command\UnlockBackendCommand:
+    tags:
+      - { name: 'console.command', command: 'backend:unlock' }
+
+  TYPO3\CMS\Backend\Command\ReferenceIndexUpdateCommand:
+    tags:
+      - { name: 'console.command', command: 'referenceindex:update' }
+
   # Temporary workaround until testing framework loads EXT:fluid in functional tests
   # @todo: Fix typo3/testing-framework and remove this
   TYPO3\CMS\Backend\View\BackendTemplateView:
diff --git a/typo3/sysext/core/Classes/Command/SiteListCommand.php b/typo3/sysext/core/Classes/Command/SiteListCommand.php
index c0a51d4156e0f280429bb7c4efc1152c87277908..c2fd02ea21b87d6cdf9491910e5e6435525d2c82 100644
--- a/typo3/sysext/core/Classes/Command/SiteListCommand.php
+++ b/typo3/sysext/core/Classes/Command/SiteListCommand.php
@@ -28,6 +28,17 @@ use TYPO3\CMS\Core\Site\SiteFinder;
  */
 class SiteListCommand extends Command
 {
+    /**
+     * @var SiteFinder
+     */
+    protected $siteFinder;
+
+    public function __construct(SiteFinder $siteFinder)
+    {
+        $this->siteFinder = $siteFinder;
+        parent::__construct();
+    }
+
     /**
      * Defines the allowed options for this command
      */
@@ -44,8 +55,7 @@ class SiteListCommand extends Command
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $io = new SymfonyStyle($input, $output);
-        $siteFinder = new SiteFinder();
-        $sites = $siteFinder->getAllSites();
+        $sites = $this->siteFinder->getAllSites();
 
         if (empty($sites)) {
             $io->title('No sites configured');
diff --git a/typo3/sysext/core/Classes/Console/CommandApplication.php b/typo3/sysext/core/Classes/Console/CommandApplication.php
index 3492572060ba3929aaa9ca44371bfdab0b0f5f9c..8b8a86c0dcb1343abc59a51ba7e2d48500d859f7 100644
--- a/typo3/sysext/core/Classes/Console/CommandApplication.php
+++ b/typo3/sysext/core/Classes/Console/CommandApplication.php
@@ -29,7 +29,6 @@ use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Information\Typo3Version;
 use TYPO3\CMS\Core\Localization\LanguageService;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Entry point for the TYPO3 Command Line for Commands
@@ -42,15 +41,21 @@ class CommandApplication implements ApplicationInterface
      */
     protected $context;
 
+    /**
+     * @var CommandRegistry
+     */
+    protected $commandRegistry;
+
     /**
      * Instance of the symfony application
      * @var Application
      */
     protected $application;
 
-    public function __construct(Context $context)
+    public function __construct(Context $context, CommandRegistry $commandRegistry)
     {
         $this->context = $context;
+        $this->commandRegistry = $commandRegistry;
         $this->checkEnvironmentOrDie();
         $this->application = new Application('TYPO3 CMS', sprintf(
             '%s (Application Context: <comment>%s</comment>)',
@@ -58,6 +63,7 @@ class CommandApplication implements ApplicationInterface
             Environment::getContext()
         ));
         $this->application->setAutoExit(false);
+        $this->application->setCommandLoader($commandRegistry);
     }
 
     /**
@@ -113,11 +119,12 @@ class CommandApplication implements ApplicationInterface
 
     /**
      * Put all available commands inside the application
+     *
+     * Note: This method will be removed in TYPO3 v11 when support for Configuration/Commands.php is dropped.
      */
     protected function populateAvailableCommands(): void
     {
-        $commands = GeneralUtility::makeInstance(CommandRegistry::class);
-        foreach ($commands as $commandName => $command) {
+        foreach ($this->commandRegistry->getLegacyCommands() as $commandName => $command) {
             /** @var Command $command */
             $this->application->add($command);
         }
diff --git a/typo3/sysext/core/Classes/Console/CommandRegistry.php b/typo3/sysext/core/Classes/Console/CommandRegistry.php
index ff60a1561c8cd9431cf4cd873258e16f33a6c5c0..969a1be4f09ee9599d317d9206124a2dd94d9159 100644
--- a/typo3/sysext/core/Classes/Console/CommandRegistry.php
+++ b/typo3/sysext/core/Classes/Console/CommandRegistry.php
@@ -15,7 +15,10 @@ namespace TYPO3\CMS\Core\Console;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
+use Symfony\Component\Console\Exception\CommandNotFoundException;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -23,17 +26,22 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 /**
  * Registry for Symfony commands, populated from extensions
  */
-class CommandRegistry implements \IteratorAggregate, SingletonInterface
+class CommandRegistry implements CommandLoaderInterface, \IteratorAggregate, SingletonInterface
 {
     /**
      * @var PackageManager
      */
     protected $packageManager;
 
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
     /**
      * Map of commands
      *
-     * @var Command[]
+     * @var array
      */
     protected $commands = [];
 
@@ -44,21 +52,68 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface
      */
     protected $commandConfigurations = [];
 
+    /**
+     * Map of lazy (DI-managed) command configurations with the command name as key
+     *
+     * @var array
+     */
+    protected $lazyCommandConfigurations = [];
+
     /**
      * @param PackageManager $packageManager
+     * @param ContainerInterface $container
+     */
+    public function __construct(PackageManager $packageManager, ContainerInterface $container)
+    {
+        $this->packageManager = $packageManager;
+        $this->container = $container;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function has($name)
+    {
+        $this->populateCommandsFromPackages();
+
+        return array_key_exists($name, $this->commands);
+    }
+
+    /**
+     * {@inheritdoc}
      */
-    public function __construct(PackageManager $packageManager = null)
+    public function get($name)
     {
-        $this->packageManager = $packageManager ?: GeneralUtility::makeInstance(PackageManager::class);
+        try {
+            return $this->getCommandByIdentifier($name);
+        } catch (UnknownCommandException $e) {
+            throw new CommandNotFoundException($e->getMessage(), [], 1567969355, $e);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getNames()
+    {
+        $this->populateCommandsFromPackages();
+
+        return array_keys($this->commands);
     }
 
     /**
      * @return \Generator
+     * @deprecated will be removed in TYPO3 v11.0 when support for Configuration/Commands.php is dropped.
      */
     public function getIterator(): \Generator
     {
+        trigger_error('Using ' . self::class . ' as iterable has been deprecated and will stop working in TYPO3 11.0.', E_USER_DEPRECATED);
+
         $this->populateCommandsFromPackages();
         foreach ($this->commands as $commandName => $command) {
+            if (is_string($command)) {
+                $command = $this->getInstance($command, $commandName);
+            }
             yield $commandName => $command;
         }
     }
@@ -73,11 +128,30 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface
         $this->populateCommandsFromPackages();
         foreach ($this->commands as $commandName => $command) {
             if ($this->commandConfigurations[$commandName]['schedulable'] ?? true) {
+                if (is_string($command)) {
+                    $command = $this->getInstance($command, $commandName);
+                }
                 yield $commandName => $command;
             }
         }
     }
 
+    /**
+     * @return \Generator
+     * @internal This method will be removed in TYPO3 v11 when support for Configuration/Commands.php is dropped.
+     */
+    public function getLegacyCommands(): \Generator
+    {
+        $this->populateCommandsFromPackages();
+        foreach ($this->commands as $commandName => $command) {
+            // Type string indicates lazy loading
+            if (is_string($command)) {
+                continue;
+            }
+            yield $commandName => $command;
+        }
+    }
+
     /**
      * @param string $identifier
      * @throws CommandNameAlreadyInUseException
@@ -95,7 +169,12 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface
             );
         }
 
-        return $this->commands[$identifier] ?? null;
+        $command = $this->commands[$identifier] ?? null;
+        if (is_string($command)) {
+            $command = $this->getInstance($command, $identifier);
+        }
+
+        return $command;
     }
 
     /**
@@ -118,6 +197,13 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface
         if ($this->commands) {
             return;
         }
+
+        foreach ($this->lazyCommandConfigurations as $commandName => $commandConfig) {
+            // Lazy commands shall be loaded from the Container on demand, store the command as string to indicate lazy loading
+            $this->commands[$commandName] = $commandConfig['class'];
+            $this->commandConfigurations[$commandName] = $commandConfig;
+        }
+
         foreach ($this->packageManager->getActivePackages() as $package) {
             $commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php';
             if (@is_file($commandsOfExtension)) {
@@ -129,6 +215,14 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface
                 $commands = require $commandsOfExtension;
                 if (is_array($commands)) {
                     foreach ($commands as $commandName => $commandConfig) {
+                        if (array_key_exists($commandName, $this->lazyCommandConfigurations)) {
+                            // Lazy (DI managed) commands override classic commands from Configuration/Commands.php
+                            // Skip this case to allow extensions to provide commands via DI config and to allow
+                            // TYPO3 v9 backwards compatibile confguration via Configuration/Commands.php.
+                            // Note: Also the deprecation error is skipped on-demand as the extension has been
+                            // adapted and the configuration will be ignored as of TYPO3 v11.
+                            continue;
+                        }
                         if (array_key_exists($commandName, $this->commands)) {
                             throw new CommandNameAlreadyInUseException(
                                 'Command "' . $commandName . '" registered by "' . $package->getPackageKey() . '" is already in use',
@@ -137,9 +231,38 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface
                         }
                         $this->commands[$commandName] = GeneralUtility::makeInstance($commandConfig['class'], $commandName);
                         $this->commandConfigurations[$commandName] = $commandConfig;
+
+                        trigger_error(
+                            'Registering console commands in Configuration/Commands.php has been deprecated and will stop working in TYPO3 v11.0.',
+                            E_USER_DEPRECATED
+                        );
                     }
                 }
             }
         }
     }
+
+    protected function getInstance(string $class, string $commandName): Command
+    {
+        $command = $this->container->get($class);
+
+        if ($command instanceof Command) {
+            $command->setName($commandName);
+            return $command;
+        }
+
+        throw new \InvalidArgumentException('Registered console command class ' . get_class($command) . ' does not inherit from ' . Command::class, 1567966448);
+    }
+
+    /**
+     * @internal
+     */
+    public function addLazyCommand(string $commandName, string $serviceName, bool $alias = false, bool $schedulable = true): void
+    {
+        $this->lazyCommandConfigurations[$commandName] = [
+            'class' => $serviceName,
+            'alias' => $alias,
+            'schedulable' => $schedulable,
+        ];
+    }
 }
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php b/typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php
new file mode 100644
index 0000000000000000000000000000000000000000..f35cc5cd9d4cf3fe6f2b1289557ae8c886fd20c7
--- /dev/null
+++ b/typo3/sysext/core/Classes/DependencyInjection/ConsoleCommandPass.php
@@ -0,0 +1,67 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use TYPO3\CMS\Core\Console\CommandRegistry;
+
+/**
+ * @internal
+ */
+final class ConsoleCommandPass implements CompilerPassInterface
+{
+    /**
+     * @var string
+     */
+    private $tagName;
+
+    /**
+     * @param string $tagName
+     */
+    public function __construct(string $tagName)
+    {
+        $this->tagName = $tagName;
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     */
+    public function process(ContainerBuilder $container)
+    {
+        $commandRegistryDefinition = $container->findDefinition(CommandRegistry::class);
+        if (!$commandRegistryDefinition) {
+            return;
+        }
+
+        $unorderedEventListeners = [];
+        foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
+            $container->findDefinition($serviceName)->setPublic(true);
+            foreach ($tags as $attributes) {
+                if (!isset($attributes['command'])) {
+                    continue;
+                }
+
+                $commandRegistryDefinition->addMethodCall('addLazyCommand', [
+                    $attributes['command'],
+                    $serviceName,
+                    (bool)($attributes['alias'] ?? false),
+                    (bool)($attributes['schedulable'] ?? true)
+                ]);
+            }
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php
index 481067386562d3979f670c75ac4a3d7a53108daf..959b20feaa44dfbf1e5a37ffa8ea2a75471ed560 100644
--- a/typo3/sysext/core/Classes/ServiceProvider.php
+++ b/typo3/sysext/core/Classes/ServiceProvider.php
@@ -35,6 +35,7 @@ class ServiceProvider extends AbstractServiceProvider
         return [
             Cache\CacheManager::class => [ static::class, 'getCacheManager' ],
             Console\CommandApplication::class => [ static::class, 'getConsoleCommandApplication' ],
+            Console\CommandRegistry::class => [ static::class, 'getConsoleCommandRegistry' ],
             Context\Context::class => [ static::class, 'getContext' ],
             EventDispatcher\EventDispatcher::class => [ static::class, 'getEventDispatcher' ],
             EventDispatcher\ListenerProvider::class => [ static::class, 'getEventListenerProvider' ],
@@ -77,7 +78,15 @@ class ServiceProvider extends AbstractServiceProvider
 
     public static function getConsoleCommandApplication(ContainerInterface $container): Console\CommandApplication
     {
-        return new Console\CommandApplication($container->get(Context\Context::class));
+        return new Console\CommandApplication(
+            $container->get(Context\Context::class),
+            $container->get(Console\CommandRegistry::class)
+        );
+    }
+
+    public static function getConsoleCommandRegistry(ContainerInterface $container): Console\CommandRegistry
+    {
+        return new Console\CommandRegistry($container->get(Package\PackageManager::class), $container);
     }
 
     public static function getEventDispatcher(ContainerInterface $container): EventDispatcher\EventDispatcher
diff --git a/typo3/sysext/core/Configuration/Commands.php b/typo3/sysext/core/Configuration/Commands.php
deleted file mode 100644
index 15028c8ccf4eab8feb5bc2717457f075096fb845..0000000000000000000000000000000000000000
--- a/typo3/sysext/core/Configuration/Commands.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-return [
-    'dumpautoload' => [
-        'class' => \TYPO3\CMS\Core\Command\DumpAutoloadCommand::class,
-        'schedulable' => false,
-    ],
-    'mailer:spool:send' => [
-        'class' => \TYPO3\CMS\Core\Command\SendEmailCommand::class,
-    ],
-    'extension:list' => [
-        'class' => \TYPO3\CMS\Core\Command\ExtensionListCommand::class,
-        'schedulable' => false
-    ],
-    'site:list' => [
-        'class' => \TYPO3\CMS\Core\Command\SiteListCommand::class,
-        'schedulable' => false
-    ],
-    'site:show' => [
-        'class' => \TYPO3\CMS\Core\Command\SiteShowCommand::class,
-        'schedulable' => false
-    ]
-];
diff --git a/typo3/sysext/core/Configuration/Services.php b/typo3/sysext/core/Configuration/Services.php
index dc3ef1b92d43de7af285b98e426b1a1811e1345c..0c5420a9d0921049bff470b05287d2ec98cb158d 100644
--- a/typo3/sysext/core/Configuration/Services.php
+++ b/typo3/sysext/core/Configuration/Services.php
@@ -21,5 +21,6 @@ return function (ContainerConfigurator $container, ContainerBuilder $containerBu
     $containerBuilder->addCompilerPass(new DependencyInjection\ListenerProviderPass('event.listener'));
     $containerBuilder->addCompilerPass(new DependencyInjection\PublicServicePass('typo3.middleware'));
     $containerBuilder->addCompilerPass(new DependencyInjection\PublicServicePass('typo3.request_handler'));
+    $containerBuilder->addCompilerPass(new DependencyInjection\ConsoleCommandPass('console.command'));
     $containerBuilder->addCompilerPass(new DependencyInjection\AutowireInjectMethodsPass());
 };
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index bd6e57342068d90f695b515e90e89d83c4b23cc5..5ac89f353670695b3d3ca657ac4d6183cee7b348 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -10,6 +10,48 @@ services:
   TYPO3\CMS\Core\DependencyInjection\EnvVarProcessor:
     tags: ['container.env_var_processor']
 
+  TYPO3\CMS\Core\Command\DumpAutoloadCommand:
+    tags:
+      - name: 'console.command'
+        command: 'dumpautoload'
+        schedulable: false
+      - name: 'console.command'
+        command: 'extensionmanager:extension:dumpclassloadinginformation'
+        alias: true
+        schedulable: false
+      - name: 'console.command'
+        command: 'extension:dumpclassloadinginformation'
+        alias: true
+        schedulable: false
+
+  TYPO3\CMS\Core\Command\ExtensionListCommand:
+    tags:
+      - name: 'console.command'
+        command: 'extension:list'
+        schedulable: false
+
+  TYPO3\CMS\Core\Command\SendEmailCommand:
+    tags:
+      - name: 'console.command'
+        command: 'mailer:spool:send'
+      - name: 'console.command'
+        command: 'swiftmailer:spool:send'
+        alias: true
+        schedulable: false
+
+
+  TYPO3\CMS\Core\Command\SiteListCommand:
+    tags:
+      - name: 'console.command'
+        command: 'site:list'
+        schedulable: false
+
+  TYPO3\CMS\Core\Command\SiteShowCommand:
+    tags:
+      - name: 'console.command'
+        command: 'site:show'
+        schedulable: false
+
   TYPO3\CMS\Core\Configuration\SiteConfiguration:
     arguments:
       $configPath: "%env(TYPO3:configPath)%/sites"
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4e373e7d45d65f6d8088fb36e516370303c7eed3
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst
@@ -0,0 +1,73 @@
+.. include:: ../../Includes.txt
+
+=====================================================================================
+Deprecation: #89139 - Console Commands configuration migrated to Symfony service tags
+=====================================================================================
+
+See :issue:`89139`
+
+Description
+===========
+
+The console command configuration file format :php:`Configuration/Commands.php`
+has been deprecated in favor of the dependency injection service tag
+`console.command`. The tag allows to configure dependency injection and
+command registration in one single location.
+
+
+Impact
+======
+
+Providing a command configuration in :php:`Configuration/Commands.php` will
+trigger a deprecation warning when the respective commands have not already
+been defined via dependency injection service tags.
+
+Extensions that provide both, the deprecated configuration file and service
+tags, will not trigger a deprecation message in order to allow extensions to
+support multiple TYPO3 major versions.
+
+
+Affected Installations
+======================
+
+TYPO3 installations with custom extensions that configure symfony console commands
+via :php:`Configuration/Commands.php` and have not been migrated to add symfony
+service tags.
+
+
+Migration
+=========
+
+Add the `console.command` tag to command classes. Use the tag attribute `command`
+to specify the command name. The optional tag attribute `schedulable` may be set
+to false to exclude the command from the TYPO3 scheduler.
+
+.. code-block:: yaml
+
+    services:
+      _defaults:
+        autowire: true
+        autoconfigure: true
+        public: false
+
+      MyVendor\MyExt\Commands\FooCommand
+        tags:
+          - name: 'console.command',
+            command: 'my:command'
+            schedulable: false
+
+Command aliases are to be configured as separate tags.
+The optonal tag attribute `alias` should be set to true for alias commands.
+
+.. code-block:: yaml
+
+      MyVendor\MyExt\Commands\BarCommand
+        tags:
+          - name: 'console.command'
+            command: 'my:bar' }
+          - name: 'console.command'
+            command: 'my:old-bar-command'
+            alias: true
+            schedulable: false
+
+.. index:: CLI, PHP-API, PartiallyScanned, ext:core
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3a981cd0cf2bebca43aa932c1b93fb5274ec9afd
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst
@@ -0,0 +1,73 @@
+.. include:: ../../Includes.txt
+
+=======================================================================
+Feature: #89139 - Add dependency injection support for console commands
+=======================================================================
+
+See :issue:`89139`
+
+Description
+===========
+
+Support for dependency injection in console commands has been added.
+
+Command dependencies can now be injected via constructor or other injection techniques.
+Therefore a new dependency injection tag `console.command` has been added.
+Commands tagged with `console.command` are lazy loaded. That means they will only be
+instantiated when they are actually executed, when the `help` subcommand is executed,
+or when available schedulable commands are iterated.
+
+The legacy command definition format :php:`Confguration/Commands.php` has been deprecated.
+
+
+Impact
+======
+
+It is recommended to configure dependency injection tags for all commands, as the legacy command
+definition format :php:`Confguration/Commands.php` has been deprecated.
+
+Commands that have been configured via `console.command` tag  override legacy commands from
+:php:`Confguration/Commands.php` without throwing a deprecation error for those commands.
+Backwards compatibility with older TYPO3 version can be achieved by specifying both variants,
+legacy configuration in :php:`Confguration/Commands.php` and new configuration via
+`console.command` tag.
+
+
+Usage
+=====
+
+Add the `console.command` tag to command classes.
+Use the tag attribute `command` to specify the command name.
+The optional tag attribute `schedulable` may be set to false
+to exclude the command from the TYPO3 scheduler.
+
+.. code-block:: yaml
+
+    services:
+      _defaults:
+        autowire: true
+        autoconfigure: true
+        public: false
+
+      MyVendor\MyExt\Commands\FooCommand
+        tags:
+          - name: 'console.command'
+            command: 'my:command'
+            schedulable: false
+
+Command aliases are to be configured as separate tags.
+The optonal tag attribute `alias` should be set to true for alias commands.
+
+.. code-block:: yaml
+
+      MyVendor\MyExt\Commands\BarCommand
+        tags:
+          - name: 'console.command'
+            command: 'my:bar'
+          - name: 'console.command'
+            command: 'my:old-bar-command'
+            alias: true
+            schedulable: false
+
+
+.. index:: CLI, PHP-API, ext:core
diff --git a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php
index 0d57be61dcaf99b43a3dac35b8f48231fc0d2432..aa301cb9b658967a46331be3e4195a0e50a6a388 100644
--- a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php
+++ b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php
@@ -16,11 +16,10 @@ namespace TYPO3\CMS\Core\Tests\Unit\Console;
  */
 
 use org\bovigo\vfs\vfsStream;
-use Prophecy\Prophecy\ObjectProphecy;
+use Psr\Container\ContainerInterface;
 use Symfony\Component\Console\Command\Command;
-use TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException;
+use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
 use TYPO3\CMS\Core\Console\CommandRegistry;
-use TYPO3\CMS\Core\Console\UnknownCommandException;
 use TYPO3\CMS\Core\Package\PackageInterface;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
@@ -40,112 +39,97 @@ class CommandRegistryTest extends UnitTestCase
      */
     protected $packageManagerProphecy;
 
+    /**
+     * @var ContainerInterface|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $containerProphecy;
+
     /**
      * Set up this testcase
      */
     protected function setUp(): void
     {
         parent::setUp();
-        $commandMockClass = $this->getMockClass(Command::class, ['dummy']);
-        $this->rootDirectory = vfsStream::setup('root', null, [
-            'package1' => [
-                'Configuration' => [
-                    'Commands.php' => '<?php return ["first:command" => [ "class" => "' . $commandMockClass . '" ]];',
-                ],
-            ],
-            'package2' => [
-                'Configuration' => [
-                    'Commands.php' => '<?php return ["second:command" => [ "class" => "' . $commandMockClass . '" ]];',
-                ],
-            ],
-            'package3' => [
-                'Configuration' => [
-                    'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];',
-                ],
-            ],
-            'package4' => [
-                'Configuration' => [
-                    'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];',
-                ],
-            ],
-        ]);
 
         /** @var PackageManager */
         $this->packageManagerProphecy = $this->prophesize(PackageManager::class);
+        $this->packageManagerProphecy->getActivePackages()->willReturn([]);
+
+        /** @var ContainerInterface */
+        $this->containerProphecy = $this->prophesize(ContainerInterface::class);
     }
 
     /**
      * @test
      */
-    public function iteratesCommandsOfActivePackages()
+    public function implementsCommandLoaderInterface()
     {
-        /** @var PackageInterface */
-        $package1 = $this->prophesize(PackageInterface::class);
-        $package1->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/');
-        /** @var PackageInterface */
-        $package2 = $this->prophesize(PackageInterface::class);
-        $package2->getPackagePath()->willReturn($this->rootDirectory->getChild('package2')->url() . '/');
-
-        $this->packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]);
-
-        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal());
-        $commands = iterator_to_array($commandRegistry);
-
-        self::assertCount(2, $commands);
-        self::assertContainsOnlyInstancesOf(Command::class, $commands);
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        self::assertInstanceof(CommandLoaderInterface::class, $commandRegistry);
     }
 
     /**
      * @test
      */
-    public function throwsExceptionOnDuplicateCommand()
+    public function iteratesLazyCommandsOfActivePackages()
     {
-        /** @var PackageInterface */
-        $package3 = $this->prophesize(PackageInterface::class);
-        $package3->getPackagePath()->willReturn($this->rootDirectory->getChild('package3')->url() . '/');
-        /** @var PackageInterface */
-        $package4 = $this->prophesize(PackageInterface::class);
-        $package4->getPackagePath()->willReturn($this->rootDirectory->getChild('package4')->url() . '/');
-        $package4->getPackageKey()->willReturn('package4');
+        $command1MockClass = $this->getMockClass(Command::class, ['dummy']);
+        $command2MockClass = $this->getMockClass(Command::class, ['dummy']);
+
+        $this->containerProphecy->get('command1')->willReturn(new $command1MockClass);
+        $this->containerProphecy->get('command2')->willReturn(new $command2MockClass);
 
-        $this->packageManagerProphecy->getActivePackages()->willReturn([$package3->reveal(), $package4->reveal()]);
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $commandRegistry->addLazyCommand('test:command', 'command1');
+        $commandRegistry->addLazyCommand('test:command2', 'command2');
 
-        $this->expectException(CommandNameAlreadyInUseException::class);
-        $this->expectExceptionCode(1484486383);
+        $commandNames = $commandRegistry->getNames();
 
-        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal());
-        iterator_to_array($commandRegistry);
+        self::assertCount(2, $commandNames);
+        self::assertInstanceOf($command1MockClass, $commandRegistry->get('test:command'));
+        self::assertInstanceOf($command1MockClass, $commandRegistry->get('test:command2'));
     }
 
     /**
      * @test
      */
-    public function getCommandByIdentifierReturnsRegisteredCommand()
+    public function iteratesLegacyCommandsOfActivePackages()
     {
-        /** @var PackageInterface|ObjectProphecy $package */
-        $package = $this->prophesize(PackageInterface::class);
-        $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/');
-        $package->getPackageKey()->willReturn('package1');
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $commands = iterator_to_array($commandRegistry->getLegacyCommands());
 
-        $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]);
-
-        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal());
-        $command = $commandRegistry->getCommandByIdentifier('first:command');
-
-        self::assertInstanceOf(Command::class, $command);
+        self::assertCount(0, $commands);
+        self::assertContainsOnlyInstancesOf(Command::class, $commands);
     }
 
     /**
      * @test
      */
-    public function throwsUnknowCommandExceptionIfUnregisteredCommandIsRequested()
+    public function lazyCommandOverridesLegacyCommandsWithoutDeprecationError()
     {
-        $this->packageManagerProphecy->getActivePackages()->willReturn([]);
+        $commandMockClass = $this->getMockClass(Command::class, ['dummy']);
+        $this->rootDirectory = vfsStream::setup('root', null, [
+            'package1' => [
+                'Configuration' => [
+                    'Commands.php' => '<?php return ["first:command" => [ "class" => "' . $commandMockClass . '" ]];',
+                ],
+            ],
+        ]);
+        /** @var PackageInterface */
+        $package = $this->prophesize(PackageInterface::class);
+        $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/');
+
+        $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]);
+
+        $this->containerProphecy->get($commandMockClass)->willReturn(new $commandMockClass);
+
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $commandRegistry->addLazyCommand('first:command', $commandMockClass);
 
-        $this->expectException(UnknownCommandException::class);
-        $this->expectExceptionCode(1510906768);
+        $nonLazyCommands = iterator_to_array($commandRegistry->getLegacyCommands());
+        $lazyCommands = $commandRegistry->getNames();
 
-        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal());
-        $commandRegistry->getCommandByIdentifier('foo');
+        self::assertCount(0, $nonLazyCommands);
+        self::assertCount(1, $lazyCommands);
     }
 }
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..777ea211d055dc74d2b769433a77a6e4cff5a095
--- /dev/null
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Console/CommandRegistryTest.php
@@ -0,0 +1,181 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\Console;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use org\bovigo\vfs\vfsStream;
+use Prophecy\Prophecy\ObjectProphecy;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Command\Command;
+use TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException;
+use TYPO3\CMS\Core\Console\CommandRegistry;
+use TYPO3\CMS\Core\Console\UnknownCommandException;
+use TYPO3\CMS\Core\Package\PackageInterface;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Testcase for CommandRegistry
+ */
+class CommandRegistryTest extends UnitTestCase
+{
+    /**
+     * @var \org\bovigo\vfs\vfsStreamDirectory
+     */
+    protected $rootDirectory;
+
+    /**
+     * @var PackageManager|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $packageManagerProphecy;
+
+    /**
+     * @var ContainerInterface|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $containerProphecy;
+
+    /**
+     * Set up this testcase
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $commandMockClass = $this->getMockClass(Command::class, ['dummy']);
+        $this->rootDirectory = vfsStream::setup('root', null, [
+            'package1' => [
+                'Configuration' => [
+                    'Commands.php' => '<?php return ["first:command" => [ "class" => "' . $commandMockClass . '" ]];',
+                ],
+            ],
+            'package2' => [
+                'Configuration' => [
+                    'Commands.php' => '<?php return ["second:command" => [ "class" => "' . $commandMockClass . '" ]];',
+                ],
+            ],
+            'package3' => [
+                'Configuration' => [
+                    'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];',
+                ],
+            ],
+            'package4' => [
+                'Configuration' => [
+                    'Commands.php' => '<?php return ["third:command" => [ "class" => "' . $commandMockClass . '" ]];',
+                ],
+            ],
+        ]);
+
+        /** @var PackageManager */
+        $this->packageManagerProphecy = $this->prophesize(PackageManager::class);
+
+        /** @var ContainerInterface */
+        $this->containerProphecy = $this->prophesize(ContainerInterface::class);
+    }
+
+    /**
+     * @test
+     */
+    public function iteratesCommandsOfActivePackages()
+    {
+        /** @var PackageInterface */
+        $package1 = $this->prophesize(PackageInterface::class);
+        $package1->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/');
+        /** @var PackageInterface */
+        $package2 = $this->prophesize(PackageInterface::class);
+        $package2->getPackagePath()->willReturn($this->rootDirectory->getChild('package2')->url() . '/');
+
+        $this->packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]);
+
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $commands = iterator_to_array($commandRegistry);
+
+        self::assertCount(2, $commands);
+        self::assertContainsOnlyInstancesOf(Command::class, $commands);
+    }
+
+    /**
+     * @test
+     */
+    public function iteratesLegacyCommandsOfActivePackages()
+    {
+        /** @var PackageInterface */
+        $package1 = $this->prophesize(PackageInterface::class);
+        $package1->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/');
+        /** @var PackageInterface */
+        $package2 = $this->prophesize(PackageInterface::class);
+        $package2->getPackagePath()->willReturn($this->rootDirectory->getChild('package2')->url() . '/');
+
+        $this->packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]);
+
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $commands = iterator_to_array($commandRegistry->getLegacyCommands());
+
+        self::assertCount(2, $commands);
+        self::assertContainsOnlyInstancesOf(Command::class, $commands);
+    }
+
+    /**
+     * @test
+     */
+    public function throwsExceptionOnDuplicateCommand()
+    {
+        /** @var PackageInterface */
+        $package3 = $this->prophesize(PackageInterface::class);
+        $package3->getPackagePath()->willReturn($this->rootDirectory->getChild('package3')->url() . '/');
+        /** @var PackageInterface */
+        $package4 = $this->prophesize(PackageInterface::class);
+        $package4->getPackagePath()->willReturn($this->rootDirectory->getChild('package4')->url() . '/');
+        $package4->getPackageKey()->willReturn('package4');
+
+        $this->packageManagerProphecy->getActivePackages()->willReturn([$package3->reveal(), $package4->reveal()]);
+
+        $this->expectException(CommandNameAlreadyInUseException::class);
+        $this->expectExceptionCode(1484486383);
+
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        iterator_to_array($commandRegistry);
+    }
+
+    /**
+     * @test
+     */
+    public function getCommandByIdentifierReturnsRegisteredCommand()
+    {
+        /** @var PackageInterface|ObjectProphecy $package */
+        $package = $this->prophesize(PackageInterface::class);
+        $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/');
+        $package->getPackageKey()->willReturn('package1');
+
+        $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]);
+
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $command = $commandRegistry->getCommandByIdentifier('first:command');
+
+        self::assertInstanceOf(Command::class, $command);
+    }
+
+    /**
+     * @test
+     */
+    public function throwsUnknowCommandExceptionIfUnregisteredCommandIsRequested()
+    {
+        $this->packageManagerProphecy->getActivePackages()->willReturn([]);
+
+        $this->expectException(UnknownCommandException::class);
+        $this->expectExceptionCode(1510906768);
+
+        $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal(), $this->containerProphecy->reveal());
+        $commandRegistry->getCommandByIdentifier('foo');
+    }
+}
diff --git a/typo3/sysext/extensionmanager/Configuration/Commands.php b/typo3/sysext/extensionmanager/Configuration/Commands.php
deleted file mode 100644
index 22dd86b0cf21c427c744649ea40a2e2d1956b06e..0000000000000000000000000000000000000000
--- a/typo3/sysext/extensionmanager/Configuration/Commands.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-/**
- * Commands to be executed by typo3, where the key of the array
- * is the name of the command (to be called as the first argument after typo3).
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- */
-return [
-    'extension:activate' => [
-        'class' => \TYPO3\CMS\Extensionmanager\Command\ActivateExtensionCommand::class,
-        'schedulable' => false,
-    ],
-    'extension:deactivate' => [
-        'class' => \TYPO3\CMS\Extensionmanager\Command\DeactivateExtensionCommand::class,
-        'schedulable' => false,
-    ],
-];
diff --git a/typo3/sysext/extensionmanager/Configuration/Services.yaml b/typo3/sysext/extensionmanager/Configuration/Services.yaml
index c94629a9cd35463e0a79a7084e08d87611eb5cd7..9c547ab8306a25e7e01a627fccf88025dd16b0e5 100644
--- a/typo3/sysext/extensionmanager/Configuration/Services.yaml
+++ b/typo3/sysext/extensionmanager/Configuration/Services.yaml
@@ -40,3 +40,31 @@ services:
         identifier: 'legacy-slot'
         method: 'emitProcessActionsSignal'
         event: TYPO3\CMS\Extensionmanager\Event\AvailableActionsForExtensionEvent
+
+  TYPO3\CMS\Extensionmanager\Command\ActivateExtensionCommand:
+    tags:
+      - name: 'console.command'
+        command: 'extension:activate'
+        schedulable: false
+      - name: 'console.command'
+        command: 'extensionmanager:extension:install'
+        alias: true
+        schedulable: false
+      - name: 'console.command'
+        command: 'extension:install'
+        alias: true
+        schedulable: false
+
+  TYPO3\CMS\Extensionmanager\Command\DeactivateExtensionCommand:
+    tags:
+      - name: 'console.command'
+        command: 'extension:deactivate'
+        schedulable: false
+      - name: 'console.command'
+        command: 'extensionmanager:extension:uninstall'
+        alias: true
+        schedulable: false
+      - name: 'console.command'
+        command: 'extension:uninstall'
+        alias: true
+        schedulable: false
diff --git a/typo3/sysext/impexp/Configuration/Commands.php b/typo3/sysext/impexp/Configuration/Commands.php
deleted file mode 100644
index cecbaea9dec4e03541b1d0d26b364a6ebb91f79e..0000000000000000000000000000000000000000
--- a/typo3/sysext/impexp/Configuration/Commands.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-/**
- * Commands to be executed by typo3, where the key of the array
- * is the name of the command (to be called as the first argument after typo3).
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- */
-return [
-    'impexp:import' => [
-        'class' => \TYPO3\CMS\Impexp\Command\ImportCommand::class
-    ]
-];
diff --git a/typo3/sysext/impexp/Configuration/Services.yaml b/typo3/sysext/impexp/Configuration/Services.yaml
index 4099126b0450823b4e91fee7841d40c4c8af4bb1..321ccd27139c8f0f891badbcfdf29659473c87d0 100644
--- a/typo3/sysext/impexp/Configuration/Services.yaml
+++ b/typo3/sysext/impexp/Configuration/Services.yaml
@@ -10,6 +10,11 @@ services:
   TYPO3\CMS\Impexp\Utility\ImportExportUtility:
     public: true
 
+  TYPO3\CMS\Impexp\Command\ImportCommand:
+    tags:
+      - name: 'console.command'
+        command: 'impexp:import'
+
   # Listener for old Signal Slots
   TYPO3\CMS\Impexp\Compatibility\SlotReplacement:
     tags:
diff --git a/typo3/sysext/install/Classes/ServiceProvider.php b/typo3/sysext/install/Classes/ServiceProvider.php
index 0f9475d2248bb8e91fa70fb561fc99acdd8610f9..a1874db01e6f8c9cac491a14e8193300683e9d3b 100644
--- a/typo3/sysext/install/Classes/ServiceProvider.php
+++ b/typo3/sysext/install/Classes/ServiceProvider.php
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Install;
 
 use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Console\CommandRegistry;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
@@ -45,6 +46,16 @@ class ServiceProvider extends AbstractServiceProvider
             Service\LoadTcaService::class => [ static::class, 'getLoadTcaService' ],
             Middleware\Maintenance::class => [ static::class, 'getMaintenanceMiddleware' ],
             Controller\UpgradeController::class => [ static::class, 'getUpgradeController' ],
+            Command\LanguagePackCommand::class => [ static::class, 'getLanguagePackCommand' ],
+            Command\UpgradeWizardRunCommand::class => [ static::class, 'getUpgradeWizardRunCommand' ],
+            Command\UpgradeWizardListCommand::class => [ static::class, 'getUpgradeWizardListCommand' ],
+        ];
+    }
+
+    public function getExtensions(): array
+    {
+        return [
+            CommandRegistry::class => [ static::class, 'configureCommands' ],
         ];
     }
 
@@ -101,4 +112,27 @@ class ServiceProvider extends AbstractServiceProvider
             $container->get(Service\LateBootService::class)
         );
     }
+
+    public static function getLanguagePackCommand(ContainerInterface $container): Command\LanguagePackCommand
+    {
+        return new Command\LanguagePackCommand;
+    }
+
+    public static function getUpgradeWizardRunCommand(ContainerInterface $container): Command\UpgradeWizardRunCommand
+    {
+        return new Command\UpgradeWizardRunCommand;
+    }
+
+    public static function getUpgradeWizardListCommand(ContainerInterface $container): Command\UpgradeWizardListCommand
+    {
+        return new Command\UpgradeWizardListCommand;
+    }
+
+    public static function configureCommands(ContainerInterface $container, CommandRegistry $commandRegistry): CommandRegistry
+    {
+        $commandRegistry->addLazyCommand('language:update', Command\LanguagePackCommand::class);
+        $commandRegistry->addLazyCommand('upgrade:run', Command\UpgradeWizardRunCommand::class);
+        $commandRegistry->addLazyCommand('upgrade:list', Command\UpgradeWizardListCommand::class);
+        return $commandRegistry;
+    }
 }
diff --git a/typo3/sysext/install/Configuration/Commands.php b/typo3/sysext/install/Configuration/Commands.php
deleted file mode 100644
index 872a35613cee2a018ccc44fbe8dfb3b00473150c..0000000000000000000000000000000000000000
--- a/typo3/sysext/install/Configuration/Commands.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-/**
- * Commands to be executed by typo3, where the key of the array
- * is the name of the command (to be called as the first argument after typo3).
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- *
- * example: bin/typo3 language:update
- */
-return [
-    'language:update' => [
-        'class' => \TYPO3\CMS\Install\Command\LanguagePackCommand::class
-    ],
-    'upgrade:run' => [
-        'class' => \TYPO3\CMS\Install\Command\UpgradeWizardRunCommand::class
-    ],
-    'upgrade:list' => [
-        'class' => \TYPO3\CMS\Install\Command\UpgradeWizardListCommand::class
-    ]
-];
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
index f909a31af43fd5e374e5353b0e745b5c2299603c..1f0cf4973a1a3f28e3249d3ba73bcbca3aa7700a 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
@@ -4420,4 +4420,12 @@ return [
             'Deprecation-90258-SimplifiedRTEParserAPI.rst',
         ],
     ],
+    'TYPO3\CMS\Core\Console\CommandRegistry->getIterator' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Feature-89139-AddDependencyInjectionSupportForConsoleCommands.rst',
+            'Deprecation-89139-ConsoleCommandsConfigurationMigratedToSymfonyServiceTags.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/lowlevel/Configuration/Commands.php b/typo3/sysext/lowlevel/Configuration/Commands.php
deleted file mode 100644
index 3b468e21a263205ee4b0f2a75ff0e434965c5434..0000000000000000000000000000000000000000
--- a/typo3/sysext/lowlevel/Configuration/Commands.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-/**
- * Commands to be executed by the typo3 CLI binary, where the key of the array
- * is the name of the command (to be called as the first argument after "bin/typo3").
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- */
-return [
-    'syslog:list' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\ListSysLogCommand::class
-    ],
-    'cleanup:missingfiles' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\MissingFilesCommand::class
-    ],
-    'cleanup:lostfiles' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\LostFilesCommand::class
-    ],
-    'cleanup:multiplereferencedfiles' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand::class
-    ],
-    'cleanup:missingrelations' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\MissingRelationsCommand::class
-    ],
-    'cleanup:deletedrecords' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand::class
-    ],
-    'cleanup:orphanrecords' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\OrphanRecordsCommand::class
-    ],
-    'cleanup:flexforms' => [
-        'class' => \TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand::class,
-    ]
-];
diff --git a/typo3/sysext/lowlevel/Configuration/Services.yaml b/typo3/sysext/lowlevel/Configuration/Services.yaml
index 6ded070b54e9a8c3e7c63777cea35c50120bd7b0..a3f390874b98dfaa9967683db060b2786fd936d1 100644
--- a/typo3/sysext/lowlevel/Configuration/Services.yaml
+++ b/typo3/sysext/lowlevel/Configuration/Services.yaml
@@ -6,3 +6,43 @@ services:
 
   TYPO3\CMS\Lowlevel\:
     resource: '../Classes/*'
+
+  TYPO3\CMS\Lowlevel\Command\ListSysLogCommand:
+    tags:
+      - name: 'console.command'
+        command: 'syslog:list'
+
+  TYPO3\CMS\Lowlevel\Command\MissingFilesCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:missingfiles'
+
+  TYPO3\CMS\Lowlevel\Command\LostFilesCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:lostfiles'
+
+  TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:multiplereferencedfiles'
+
+  TYPO3\CMS\Lowlevel\Command\MissingRelationsCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:missingrelations'
+
+  TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:deletedrecords'
+
+  TYPO3\CMS\Lowlevel\Command\OrphanRecordsCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:orphanrecords'
+
+  TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:flexforms'
diff --git a/typo3/sysext/redirects/Configuration/Commands.php b/typo3/sysext/redirects/Configuration/Commands.php
deleted file mode 100644
index b9687f07e55fd8d8b76e5f4df04991adcd362a77..0000000000000000000000000000000000000000
--- a/typo3/sysext/redirects/Configuration/Commands.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-/**
- * Commands to be executed by typo3, where the key of the array
- * is the name of the command (to be called as the first argument after typo3).
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- */
-return [
-    'redirects:checkintegrity' => [
-        'class' => \TYPO3\CMS\Redirects\Command\CheckIntegrityCommand::class,
-        'schedulable' => true,
-    ],
-];
diff --git a/typo3/sysext/redirects/Configuration/Services.yaml b/typo3/sysext/redirects/Configuration/Services.yaml
index 17372701f400c95078d56ec0ea3d05a8f1f3f94c..eb29c25540e054035896f96b20582269d7fad3f0 100644
--- a/typo3/sysext/redirects/Configuration/Services.yaml
+++ b/typo3/sysext/redirects/Configuration/Services.yaml
@@ -13,6 +13,11 @@ services:
   TYPO3\CMS\Redirects\Hooks\DataHandlerSlugUpdateHook:
     public: true
 
+  TYPO3\CMS\Redirects\Command\CheckIntegrityCommand:
+    tags:
+      - name: 'console.command'
+        command: 'redirects:checkintegrity'
+
   TYPO3\CMS\Redirects\EventListener\RecordHistoryRollbackEventsListener:
     tags:
       - name: event.listener
diff --git a/typo3/sysext/scheduler/Configuration/Commands.php b/typo3/sysext/scheduler/Configuration/Commands.php
deleted file mode 100644
index d6d3aa2fe051af9284eadbff8dd1f3822f78a6e8..0000000000000000000000000000000000000000
--- a/typo3/sysext/scheduler/Configuration/Commands.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-/**
- * Commands to be executed by typo3, where the key of the array
- * is the name of the command (to be called as the first argument after typo3).
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- */
-return [
-    'scheduler:run' => [
-        'class' => \TYPO3\CMS\Scheduler\Command\SchedulerCommand::class,
-        // command must not be schedulable, otherwise we'll get an endless recursion
-        'schedulable' => false,
-    ]
-];
diff --git a/typo3/sysext/scheduler/Configuration/Services.yaml b/typo3/sysext/scheduler/Configuration/Services.yaml
index c719171541e8ba1cfde0467afba4cae5485b79aa..b7456c24a3d60263aa123eea56f90eb7862dda60 100644
--- a/typo3/sysext/scheduler/Configuration/Services.yaml
+++ b/typo3/sysext/scheduler/Configuration/Services.yaml
@@ -7,6 +7,12 @@ services:
   TYPO3\CMS\Scheduler\:
     resource: '../Classes/*'
 
+  TYPO3\CMS\Scheduler\Command\SchedulerCommand:
+    tags:
+      - name: 'console.command'
+        command: 'scheduler:run'
+        schedulable: false
+
   TYPO3\CMS\Scheduler\SystemInformation\ToolbarItemProvider:
     tags:
       - name: event.listener
diff --git a/typo3/sysext/workspaces/Configuration/Commands.php b/typo3/sysext/workspaces/Configuration/Commands.php
deleted file mode 100644
index e156d33fa66cfa741c05e8400763629b65edfeb7..0000000000000000000000000000000000000000
--- a/typo3/sysext/workspaces/Configuration/Commands.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/**
- * Commands to be executed by the typo3 CLI binary, where the key of the array
- * is the name of the command (to be called as the first argument after "bin/typo3").
- * Required parameter is the "class" of the command which needs to be a subclass
- * of Symfony/Console/Command.
- */
-return [
-    'cleanup:versions' => [
-        'class' => \TYPO3\CMS\Workspaces\Command\WorkspaceVersionRecordsCommand::class,
-    ],
-    'cleanup:previewlinks' => [
-        'class' => \TYPO3\CMS\Workspaces\Command\CleanupPreviewLinksCommand::class,
-    ],
-    'workspace:autopublish' => [
-        'class' => \TYPO3\CMS\Workspaces\Command\AutoPublishCommand::class,
-    ],
-];
diff --git a/typo3/sysext/workspaces/Configuration/Services.yaml b/typo3/sysext/workspaces/Configuration/Services.yaml
index 9c62f633de7cbe600cf03e5cf8d68873a5ffc1c8..2649612811def30f370d5e2807d5000cc1485d09 100644
--- a/typo3/sysext/workspaces/Configuration/Services.yaml
+++ b/typo3/sysext/workspaces/Configuration/Services.yaml
@@ -15,6 +15,21 @@ services:
   TYPO3\CMS\Workspaces\Service\GridDataService:
     public: true
 
+  TYPO3\CMS\Workspaces\Command\WorkspaceVersionRecordsCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:versions'
+
+  TYPO3\CMS\Workspaces\Command\CleanupPreviewLinksCommand:
+    tags:
+      - name: 'console.command'
+        command: 'cleanup:previewlinks'
+
+  TYPO3\CMS\Workspaces\Command\AutoPublishCommand:
+    tags:
+      - name: 'console.command'
+        command: 'workspace:autopublish'
+
   TYPO3\CMS\Workspaces\Compatibility\SlotReplacement:
     tags:
       - name: event.listener