diff --git a/plugin.module b/plugin.module
index fc8387a..0584ec7 100644
--- a/plugin.module
+++ b/plugin.module
@@ -5,6 +5,10 @@
  * Contains hook implementations.
  */
 
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\FieldStorageConfigInterface;
 use Drupal\plugin\Plugin\Field\FieldType\PluginCollectionItemInterface;
 
 function plugin_field_info_alter(array &$field_type_definitions) {
@@ -33,6 +37,107 @@ function plugin_field_widget_info_alter(array &$field_widget_definitions) {
 }
 
 /**
+ * Implements hook_views_data_alter().
+ */
+function plugin_views_data_alter(array &$data) {
+  /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
+  $entity_field_manager = \Drupal::service('entity_field.manager');
+
+  // We need to work with entity type database table mappings, which are
+  // available per entity type.
+  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type) {
+    // Skip non-fieldable entity types.
+    if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
+      continue;
+    }
+
+    $field_definitions = $entity_field_manager->getBaseFieldDefinitions($entity_type->id());
+    $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type->id());
+
+    // We cannot alter Views data if we cannot map fields to tables.
+    if (!($entity_storage instanceof SqlEntityStorageInterface)) {
+      continue;
+    }
+    $table_mapping = $entity_storage->getTableMapping($field_definitions);
+    if (!$table_mapping) {
+      continue;
+    }
+
+    // Loop through all of this entity type's stored fields.
+    foreach ($table_mapping->getTableNames() as $table_name) {
+      foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+        $field_definition = $field_definitions[$field_name];
+
+        // Skip this field if it's of the wrong type.
+        if (strpos($field_definition->getType(), 'plugin:') === 0) {
+          continue;
+        }
+
+        $plugin_id_column_name = $table_mapping->getFieldColumnName($field_definition->getFieldStorageDefinition(), 'plugin_id');
+
+        // Skip this field if no Views data for it exists yet.
+        if (!isset($data[$table_name][$plugin_id_column_name])) {
+          continue;
+        }
+
+        _plugin_views_data_alter_filter_plugin_id($data, $field_definition->getFieldStorageDefinition(), $table_name, $plugin_id_column_name);
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_field_views_data_alter().
+ */
+function plugin_field_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
+  // Add the "plugin_id" filter to all configurable "plugin" fields.
+  if (strpos($field_storage->getType(), 'plugin:') === 0) {
+    $table_name = $field_storage->getTargetEntityTypeId() . '__' . $field_storage->getName();
+    $plugin_id_column_name = $field_storage->getName() . '_plugin_id';
+
+    // Skip if there is no Views data for this field.
+    if (!isset($data[$table_name][$plugin_id_column_name])) {
+      return;
+    }
+
+    _plugin_views_data_alter_filter_plugin_id($data, $field_storage, $table_name, $plugin_id_column_name);
+  }
+}
+
+/**
+ * Alters Views data for the "plugin_id" Views filter plugin.
+ *
+ * @param array[] $data
+ *   An array of Views data.
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition
+ *   The storage definition of the field to alter the data for.
+ * @param $table_name
+ *   The name of the table to alter the data for.
+ * @param $plugin_id_column_name
+ *   The name of the original plugin ID column this method adds data to.
+ */
+function _plugin_views_data_alter_filter_plugin_id(array &$data, FieldStorageDefinitionInterface $field_storage_definition, $table_name, $plugin_id_column_name) {
+  $plugin_id_filter_column_name = $plugin_id_column_name . '_filter';
+  $data[$table_name][$plugin_id_filter_column_name] = [
+    'title' => t('@label (plugin ID filter)', [
+      '@label' => $field_storage_definition->getLabel(),
+    ]),
+    'group' => isset($data[$table_name][$plugin_id_column_name]['group']) ? $data[$table_name][$plugin_id_column_name]['group'] : NULL,
+    'help' => isset($data[$table_name][$plugin_id_column_name]['help']) ? $data[$table_name][$plugin_id_column_name]['help'] : NULL,
+    'filter' => [
+      'field' => $plugin_id_column_name,
+      'table' => $table_name,
+      'id' => 'plugin_id',
+      'additional fields' => [],
+      'field_name' => $field_storage_definition->getName(),
+      'entity_type' => $field_storage_definition->getTargetEntityTypeId(),
+      'plugin_type_id' => substr($field_storage_definition->getType(), 7),
+      'allow empty' => TRUE,
+    ],
+  ];
+}
+
+/**
  * Gets the IDs of plugin item collection field types.
  *
  * @return string[]
diff --git a/src/Plugin/views/filter/PluginId.php b/src/Plugin/views/filter/PluginId.php
new file mode 100644
index 0000000..25cfefd
--- /dev/null
+++ b/src/Plugin/views/filter/PluginId.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\plugin\Plugin\views\filter;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
+use Drupal\plugin\PluginType\PluginTypeInterface;
+use Drupal\views\Plugin\views\filter\InOperator;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a Views filter for plugin IDs.
+ *
+ * @ingroup views_filter_handlers
+ *
+ * @ViewsFilter("plugin_id")
+ */
+final class PluginId extends InOperator implements ContainerFactoryPluginInterface {
+
+  /**
+   * The plugin type.
+   *
+   * @var \Drupal\plugin\PluginType\PluginTypeInterface
+   */
+  protected $pluginType;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param mixed[] $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin ID.
+   * @param mixed[] $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\plugin\PluginType\PluginTypeInterface $plugin_type
+   *   The plugin type.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, PluginTypeInterface $plugin_type) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->pluginType = $plugin_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    /** @var \Drupal\plugin\PluginType\PluginTypeManagerInterface $plugin_type_manager */
+    $plugin_type_manager = $container->get('plugin.plugin_type_manager');
+
+    return new static($configuration, $plugin_id, $plugin_definition, $plugin_type_manager->getPluginType($configuration['plugin_type_id']));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValueOptions() {
+    if (!is_null($this->valueOptions)) {
+      return $this->valueOptions;
+    }
+
+    $this->valueOptions = array_reduce($this->pluginType->getPluginManager()->getDefinitions(), function(array $value_options, $plugin_definition) {
+      $plugin_definition = $this->pluginType->ensureTypedPluginDefinition($plugin_definition);
+      $value_options[$plugin_definition->getId()] = $plugin_definition instanceof PluginLabelDefinitionInterface ? $plugin_definition->getLabel() : $plugin_definition->getId();
+      return $value_options;
+    }, []);
+    natcasesort($this->valueOptions);
+
+    return $this->valueOptions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function valueForm(&$form, FormStateInterface $form_state) {
+    parent::valueForm($form, $form_state);
+    // Apply cacheability metadata, because the parent class does not.
+    $this->getCacheableMetadata()->applyTo($form);
+
+    return $form;
+  }
+
+  /**
+   * Gets this instance's cacheable metadata.
+   *
+   * @return \Drupal\Core\Cache\CacheableMetadata
+   */
+  protected function getCacheableMetadata() {
+    $cacheable_metadata = new CacheableMetadata();
+    $cacheable_metadata->addCacheableDependency($this->pluginType->getPluginManager());
+    $cacheable_metadata->addCacheTags(parent::getCacheTags());
+    $cacheable_metadata->addCacheContexts(parent::getCacheContexts());
+    $cacheable_metadata->mergeCacheMaxAge(parent::getCacheMaxAge());
+
+    return $cacheable_metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->getCacheableMetadata()->getCacheTags();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->getCacheableMetadata()->getCacheContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->getCacheableMetadata()->getCacheMaxAge();
+  }
+
+}
diff --git a/tests/src/Unit/Plugin/views/filter/PluginIdTest.php b/tests/src/Unit/Plugin/views/filter/PluginIdTest.php
new file mode 100644
index 0000000..8fbdf93
--- /dev/null
+++ b/tests/src/Unit/Plugin/views/filter/PluginIdTest.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Drupal\Tests\plugin\Unit\Plugin\views\filter;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\plugin\Plugin\views\filter\PluginId;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
+use Drupal\plugin\PluginType\PluginTypeInterface;
+use Drupal\plugin\PluginType\PluginTypeManagerInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\Plugin\views\filter\PluginId
+ *
+ * @group Plugin
+ */
+class PluginIdTest extends UnitTestCase {
+
+  /**
+   * The plugin type.
+   *
+   * @var \Drupal\plugin\PluginType\PluginTypeInterface|\Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $pluginType;
+
+  /**
+   * The system under test.
+   *
+   * @var \Drupal\plugin\Plugin\views\filter\PluginId
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $plugin_type_id = 'foo_bar';
+    $plugin_id = '';
+    $plugin_definition = [
+      'id' => $plugin_id,
+    ];
+    $configuration = [
+      'plugin_type_id' => $plugin_type_id,
+    ];
+
+    $this->pluginType = $this->prophesize(PluginTypeInterface::class);
+
+    $this->sut = new PluginId($configuration, $plugin_id, $plugin_definition, $this->pluginType->reveal());
+  }
+
+  /**
+   * @covers ::create
+   * @covers ::__construct
+   */
+  public function testCreate() {
+    $plugin_type_id = 'foo_bar';
+    $plugin_id = '';
+    $plugin_definition = [
+      'id' => $plugin_id,
+    ];
+    $configuration = [
+      'plugin_type_id' => $plugin_type_id,
+    ];
+
+    $plugin_type_manager = $this->prophesize(PluginTypeManagerInterface::class);
+    $plugin_type_manager->getPluginType($plugin_type_id)->wilLReturn($this->pluginType->reveal());
+
+    $container = $this->prophesize(ContainerInterface::class);
+    $container->get('plugin.plugin_type_manager')->willReturn($plugin_type_manager->reveal());
+
+    $this->sut = PluginId::create($container->reveal(), $configuration, $plugin_id, $plugin_definition);
+    $this->assertInstanceOf(PluginId::class, $this->sut);
+  }
+
+  /**
+   * @covers ::getCacheContexts
+   * @covers ::getCacheableMetadata
+   */
+  public function testCacheContexts() {
+    $plugin_manager_cache_contexts = ['dog', 'ball'];
+
+    $plugin_manager = $this->prophesize(CacheableDependencyPluginManagerInterface::class);
+    $plugin_manager->getCacheContexts()->willReturn($plugin_manager_cache_contexts);
+    $plugin_manager->getCacheTags()->willReturn([]);
+    $plugin_manager->getCacheMaxAge()->willReturn(0);
+
+    $this->pluginType->getPluginManager()->willReturn($plugin_manager->reveal());
+
+    // Temporarily disable asserts, because Cache::mergeContexts() calls
+    // \Drupal::service(). See https://www.drupal.org/node/2720947.
+    assert_options(ASSERT_ACTIVE, FALSE);
+    $cache_contexts = $this->sut->getCacheContexts();
+    $this->assertInternalType('array', $cache_contexts);
+    foreach ($plugin_manager_cache_contexts as $plugin_manager_cache_context) {
+      $this->assertTrue(in_array($plugin_manager_cache_context, $cache_contexts));
+    }
+    assert_options(ASSERT_ACTIVE, TRUE);
+  }
+
+  /**
+   * @covers ::getCacheTags
+   * @covers ::getCacheableMetadata
+   */
+  public function testCacheTags() {
+    $plugin_manager_cache_tags = ['bar', 'foo'];
+
+    $plugin_manager = $this->prophesize(CacheableDependencyPluginManagerInterface::class);
+    $plugin_manager->getCacheContexts()->willReturn([]);
+    $plugin_manager->getCacheTags()->willReturn($plugin_manager_cache_tags);
+    $plugin_manager->getCacheMaxAge()->willReturn(0);
+
+    $this->pluginType->getPluginManager()->willReturn($plugin_manager->reveal());
+
+    $this->assertArraySubset($plugin_manager_cache_tags, $this->sut->getCacheTags());
+  }
+
+  /**
+   * @covers ::getCacheMaxAge
+   * @covers ::getCacheableMetadata
+   *
+   * @dataProvider provideCacheMaxAge
+   */
+  public function testCacheMaxAge($expected, $plugin_manager_max_age) {
+    $plugin_manager = $this->prophesize(CacheableDependencyPluginManagerInterface::class);
+    $plugin_manager->getCacheContexts()->willReturn([]);
+    $plugin_manager->getCacheTags()->willReturn([]);
+    $plugin_manager->getCacheMaxAge()->willReturn($plugin_manager_max_age);
+
+    $this->pluginType->getPluginManager()->willReturn($plugin_manager->reveal());
+
+    $this->assertSame($expected, $this->sut->getCacheMaxAge());
+  }
+
+  /**
+   * Provides data to self::testCacheMaxAge().
+   */
+  public function provideCacheMaxAge() {
+    $data = [];
+
+    $data['plugin-manager-permanent'] = [Cache::PERMANENT, Cache::PERMANENT];
+    $data['plugin-manager-never'] = [0, 0];
+    $data['plugin-manager-limited'] = [7, 7];
+
+    return $data;
+  }
+
+  /**
+   * @covers ::getValueOptions
+   * @covers ::getCacheableMetadata
+   */
+  public function testGetValueOptions() {
+    $plugin_label_1 = 'Foo';
+    $plugin_id_1 = 'aaa_foo';
+    $plugin_id_2 = 'baz';
+    $plugin_id_3 = 'qux';
+    $plugin_label_4 = 'Bar';
+    $plugin_id_4 = 'zzz_bar';
+
+    // Values must be sorted naturally.
+    $expected = [
+      $plugin_id_4 => $plugin_label_4,
+      $plugin_id_2 => $plugin_id_2,
+      $plugin_id_1 => $plugin_label_1,
+      $plugin_id_3 => $plugin_id_3,
+    ];
+
+    $plugin_definition_1 = $this->prophesize(PluginLabelDefinitionInterface::class);
+    $plugin_definition_1->getId()->willReturn($plugin_id_1);
+    $plugin_definition_1->getLabel()->willReturn($plugin_label_1);
+    $plugin_definition_2 = $this->prophesize(PluginDefinitionInterface::class);
+    $plugin_definition_2->getId()->willReturn($plugin_id_2);
+    $plugin_definition_3 = $this->prophesize(PluginDefinitionInterface::class);
+    $plugin_definition_3->getId()->willReturn($plugin_id_3);
+    $plugin_definition_4 = $this->prophesize(PluginLabelDefinitionInterface::class);
+    $plugin_definition_4->getId()->willReturn($plugin_id_4);
+    $plugin_definition_4->getLabel()->willReturn($plugin_label_4);
+
+    $plugin_definitions = [
+      $plugin_id_1 => $plugin_definition_1,
+      $plugin_id_2 => $plugin_definition_2,
+      $plugin_id_3 => $plugin_definition_3,
+      $plugin_id_4 => $plugin_definition_4,
+    ];
+
+    $plugin_manager = $this->prophesize(PluginManagerInterface::class);
+    $plugin_manager->getDefinitions()->willReturn($plugin_definitions);
+
+    $this->pluginType->ensureTypedPluginDefinition(Argument::any())->willReturnArgument();
+    $this->pluginType->getPluginManager()->willReturn($plugin_manager);
+
+    $this->assertSame($expected, $this->sut->getValueOptions());
+  }
+
+
+}
+
+/**
+ * Defines a plugin manager which is also a cacheable dependency.
+ */
+interface CacheableDependencyPluginManagerInterface extends PluginManagerInterface, CacheableDependencyInterface {
+}
