What is the correct way to set cache contexts on custom blocks? I have run into an issue where a block that should be unique per page isn't for logged-out users.  The issue is a custom block plugin I have on a views search page that contains custom filters (sort of like a custom replacement for exposed filters. the block placed through /admin/structure/block).

Based on what I have learned about Drupal 8, I added the cache contexts to my build array:

  public function build() {

    $search_form = \Drupal::formBuilder()-&gt;getForm('Drupal\mymodule\Form\SearchForm');
    return [
      'search_form' =&gt; $search_form,
      '#cache' =&gt; ['contexts' =&gt; ['url.path', 'url.query_args']]
    ];

  }


But it seems this must be incorrect because when logged-out, the block would get cached on the first view, and when the url changed, it wasn't showing a new version of the block.

I thought it might be the view page that was causing the issue, but even when I turned caching off on the view page, the issue remained.

I was able to fix the issue several ways, for example, by using a preprocess_block hook:

function mymodule_preprocess_block__mycustomsearchblock(&amp;$variables) {
  $variables['#cache']['contexts'][] = 'url.path';
  $variables['#cache']['contexts'][] = 'url.query_args';
}


But it bothered me I couldn't just put the cache contexts into my block's build array.

Since my block extends BlockBase, I decided to try out the getCacheContexts() method, especially since I saw some modules within core are doing it this way.

  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['url.path', 'url.query_args']);
  }


This is also fixed the issue, but interestingly, when I output the variables in the preprocess block function, these are not showing in $variables['#cache']['contexts'], but they do show in $variables['elements']['#cache']['contexts']

array:5 [▼
  0 =&gt; "languages:language_interface"
  1 =&gt; "theme"
  2 =&gt; "url.path"
  3 =&gt; "url.query_args"
  4 =&gt; "user.permissions"
]


I'm trying to figure out how this works, and why it wasn't working from the build function.

Looking at /core/modules/block/src/BlockViewBuilder.php at the viewMultiple() function, it looks like it pulls the cache tags from the entity and the plugin:

'contexts' =&gt; Cache::mergeContexts(
  $entity-&gt;getCacheContexts(),
  $plugin-&gt;getCacheContexts()
),


So that explains why adding a getCacheContexts() method to my block plugin adds the contexts to my block.  Also, looking at the preRender method in the same class, it looks like it doesn't use the cache array in the blocks build function, which confuses me, as it seems the way to add caching in Drupal 8 is to add a #cache element to render elements.

So my question is, 

1) Are cache contexts added directly on the array in a block plugin ignored?

2) If so, is there a way around that, do we need to add it to a child element of the build array?

3) If context added directly is ignored, is adding a getCacheContexts() the way to go for block plugins in custom modules?
In most cases, you just set the cache context directly on the render array you return in your build() method.

I have finally found what my issue was, with the help of @Berdir and @4k4.  If you are using a custom template, such as block--myblock.html.twig and you output the variables individually, such as {{ content.foo }} instead of all at the same time like {{ content }}, it ignores your cache contexts passed directly into your block build array, when logged out.  See What is the correct way to set cache contexts on custom blocks?

So, to answer the original question:

1) Cache contexts passed directly into a custom block plugin are sometimes ignored.  You can test this by altering the SyndicateBlock, and then creating a custom template in your theme block--syndicate.html.php in which you output the variables individually like this:

{% block content %}
  {{ content.foo }}
{% endblock %}


As you change the url arguments, you'll see the block doesn't respect the cache context.

Now if you change it output all the content as a piece, it works:

{% block content %}
  {{ content }}
{% endblock %}


Now, it respects the cache context, and the block is unique per page.

2)  For now, to work around this, you can just output what's in your block in it's own template.

 public function build() {

    $search_form = \Drupal::formBuilder()-&gt;getForm('Drupal\mymodule\Form\SearchForm');
    return [
      '#theme' =&gt; 'mycustomtemplate',
      '#search_form' =&gt; $search_form,
      '#cache' =&gt; ['contexts' =&gt; ['url.path', 'url.query_args']]
    ];

  }


This circumvents the block module's esoteric caching exceptions and your form is now unique per page when logged out.

3) Should you create your own theme template to fix this, or just add a method for getCacheContexts() in your custom Block plugin?  It's better to create a new theme template, rather than add a getCacheContexts() method that overrides the natural order of bubbling up of cache contexts, and may break metadata deeper in your build array.