Lazy configuration: mapping properties without losing the task reference?

I have a task whose single Output is a properties file:

public class PropertiesProviderTask extends DefaultTask {
    @OutputFile
    private RegularFileProperty output;

    // ...
}

… and a consumer task that takes a String as input property:

public class ConsumerTask extends DefaultTask {
    @Input
    private Property<String> input;

    // ...
}

I want to use one of the values within the provided property file as input for the consumer task and configure everything as lazily as possible.

task propertiesProvider(type: PropertiesProviderTask) {
    // ...
}

task consumer(type: ConsumerTask) {
    input = propertiesProvider.getOutput().map(propertiesFile -> {
        Optional<String> value = retrieveProperty(propertiesFile, "property-key");
        return value.orElse(null);
    });
}

As far as I understand, Gradle should track the task reference and make consumer automatically depend on propertiesProducer, shouldn’t it?

https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#map-org.gradle.api.Transformer-

The documentation on map states:

When this provider represents a task or the output of a task, the new provider will be considered an output of the task and will carry dependency information that Gradle can use to automatically attach task dependencies to tasks that use the new provider for input values.

However, I’m getting the infamous:

Querying the mapped value of provider(java.util.Set) before task ‘:propertiesProvider’ has completed is not supported

unless I explicitly add:

consumer.dependsOn propertiesProducer

What am I doing wrong here?

Answering my own question here:

The consumer task above was only displaying values on the console, so there were no outputs defined for that task. As it turns out, when no outputs are defined, the aforementioned “automatically depend on” mechanism doesn’t seem to work.

I fixed it by adding

getOutputs().upToDateWhen(task -> false);

in the consumer task’s constructor, in order to force the input/output mechanisms to be engaged.

If any Gradle dev happens to be reading this: it would be great if the automatic task chaining would work without the consuming task having to declare outputs.

As your example is cut down until non-compilability, it is hard to say what might be wrong.
You definitely do not need to have that upToDateWhen.
I made your example runnable how I guessed you have it and it works perfectly fine, having the expected task dependencies.
If your examples are for example Java classes, you probably just misplaced the annotations as they must be on the getter in Java classes which is also documented in the user guide.

If you need more help, please provide a proper MCVE.

Btw. as you wrote “as lazy as possible”, well it is not about laziness, but about task configuration avoidance.
You do not use it with your tasks. Find more information at Task Configuration Avoidance.