How to create a "WhereNotContainsTags" for ContentItemQueryBuilder

I know that I can filter an Content Item query with WhereContainsTags. But would there be a way to extend the content item query so that I could add WhereNotContainsTags (or something to that effect?). The query would like this:

dfdfdvar query = new ContentItemQueryBuilder()
	.ForContentType(
		ContentDetail.CONTENT_TYPE_NAME,
		config => config
			.Where(where =>
			{
				where.WhereNotContainsTags(nameof(ContentDetail.Languages), ignoredLanguageGuids);
			})
			.ForWebsite(currentSiteName, includeUrlPath: true)
	)
	.InLanguage(currentCulture);f

Environment

  • Xperience by Kentico version: 31.7.4

  • .NET version: 8

Answers

Hi aj-hoffer,

I hit the same wall, so I actually tried this out with Dancing Goat template before answering.

First thing I tried was wrapping WhereContainsTags in WhereNot, since that's the negation method used elsewhere in Xperience's WHERE API:

subqueryParameters.Where(where =>
    where.WhereNot(new WhereCondition().WhereContainsTags(nameof(ProductCoffee.CoffeeProcessing), excludedTagGuids)));

That doesn't compile:

'WhereParameters' does not contain a definition for 'WhereNot'...
The type or namespace name 'WhereCondition' could not be found...

Turns out WhereNot/WhereCondition belong to the ObjectQuery/DataEngine WHERE API, not the WhereParameters type you get inside a content item query's .Where(...) — they look similar (share names like WhereEquals, WhereLike) but they're different builder types under the hood, and WhereNot just isn't one of the shared members.

What actually works is a two-step query: pull the IDs of items that do match the tag, then exclude them from a second query with WhereNotIn:

var matchingBuilder = new ContentItemQueryBuilder()
    .ForContentType(ProductCoffee.CONTENT_TYPE_NAME, config => config
        .Where(where => where.WhereContainsTags(nameof(ProductCoffee.CoffeeProcessing), excludedTagGuids))
        .Columns("ContentItemID"))
    .InLanguage("en");

var matchingIds = await executor.GetResult(matchingBuilder,
    c => c.GetValue<int>("ContentItemID"), cancellationToken: cancellationToken);

var resultBuilder = new ContentItemQueryBuilder()
    .ForContentType(ProductCoffee.CONTENT_TYPE_NAME, config => config
        .Where(where => where.WhereNotIn("ContentItemID", matchingIds.ToArray()))
        .Columns("ContentItemName"))
    .InLanguage("en");

var itemsWithoutTag = await executor.GetResult(resultBuilder,
    c => c.GetValue<string>("ContentItemName"), cancellationToken: cancellationToken);

I ran this against the Dancing Goat sample data, excluding the "Decaf" tag on ProductCoffee.CoffeeProcessing. Output:

excludedTag: "Decaf"
matchingIds: [52, 87]
itemsWithoutTag:
  BoliviaFincaIllimani-9nut1evb
  BrazilNaturalBarraGrande-g30f5chq
  ColombiaCarlosImbachi-6pwcdcic
  ElSalvadorFincaSanJose-ssa7ezz7
  GuatemalaFincaElInjerto-k3bwkxk3
  KenyaGakuyuniAA-j2ujcs2m
  PanamaLosLajonesHoney-oqi18lde

2 tagged + 7 untagged = 9, which matches the total ProductCoffee item count in Content Hub exactly - so items with no tag at all in that field are correctly included too, not silently dropped (that was the edge case I was most worried about).

Two queries instead of one, but it's the only version that actually compiles and returns correct results.

References:

@Pawan, thanks! This is what we ended up doing as well. I was hoping there would be a way to extend the current functionality, but it looks like we may have to wait for a way to do that.

Thanks!

Hi aj-hoffer,


Glad it converged with what you found - good to have it confirmed independently. Hopefully Kentico adds a native WhereNotContainsTags down the line so neither of us needs the two-query workaround.

Thanks

To response this discussion, you have to login first.