Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2025/mistake-satiq-pipelines

A mistake to avoid with Statiq's pipelines

How not to implement a 'do nothing' path in a module...

Published 28 July 2025
C# Statiq ~2 min. read

I've been tinkering with some pipeline modules for the Statiq generation engine that I use to render this blog, and I bumped into a fun mistake in my code which confused me for a while. To help me next time I'm doing this sort of modification, this seemed worth documenting - even if it is a bit of a silly mistake...

The issue url copied!

I was working on a module which needed to generate some custom data for specific content types in the input to my Statiq site. So I created a new Module and typed override and let the autocomplete generate the main method for this class:

public class CustomIndexModule : ParallelModule
{
    protected override Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
    {
        return base.ExecuteInputAsync(input, context);
    }
}

					

And with that in place I started to write some logic. Most of it is unimportant to this issue - but the key lines were basically:

public class CustomIndexModule : ParallelModule
{
    protected override Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
    {
        if(!input.Destination.FullPath.StartsWith("myFolder/"))
        {
            return base.ExecuteInputAsync(input, context);
        }
        
        //
        // logic to generate the index data
        //
    }
}

					

So (I thought) if the particular document isn't in the right folder, this module will skip over it and do nothing. And I added this module to the pipeline for the site.

But once this was in place I started to see some odd issues. Running the generator now threw an unexpected error for a page which had been working fine beforehand:

[ERRO] Archives/PostProcess » ExecuteSwitch » RenderContentPostProcessTemplates » ExecuteIf » ExecuteIf » RenderRazor » [C:/Users/Jeremy/source/repos/blog.jermdavis.dev/StatiqGenerator/theme/input/mvp.cshtml => mvp.html] Object reference not set to an instance of an object.

					

And commenting out the new module made this go away immediately.

I spent a bit of time fiddling here, trying to work out what had gone wrong - but the only other thing I noticed was that some files were missing from the output - including the one that was mentioned in the message above.

The solution url copied!

Having dug around for rather too long, I realised that I should look back at previous code I'd written for modules. And in doing that I had a bit of a revelation. In the code I'd written the "do nothing" behaviour was to call the base.ExecuteInputAsync() method as that's what the autocomplete had created with the empty method. (That's the default behaviour for overriding a C# method - call its base) But my previous code had called input.Yield() instead, driven by what's in the Statiq documentation. And changing the code to:

public class CustomIndexModule : ParallelModule
{
    protected async override Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
    {
        if(!input.Destination.FullPath.StartsWith("myFolder/"))
        {
            return input.Yield();
        }
    
        // rest of the logic
    }
}

					

resolved the issue. So what was up? Well digging into the code that underlies the base Module definition you see this:

protected virtual Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context) =>
            Task.FromResult<IEnumerable<IDocument>>(null);

					

The overall result of calling the base method for ExecuteInputAsync() is to return null - so my code above wasn't keeping the current input document, but was discarding it.

Hence the issue I was seeing where files were not being generated by the build. Instead of being passed through unprocessed, the documents that didn't need the custom index data were being discarded.

Hopefully this post will help me not make that mistake again. And it's a useful reminder that generally "pipeline" style code needs a different approach to doing nothing when it receives an input. You generally need to make sure you pass the input back out again rather than actually doing nothing at all...

↑ Back to top