I think a middle-tier service is perfect to illustrate how I see the strengths and weaknesses of these two. Specifically the scenario:
Complex Middle-Tier: Startup vs. Message Handling
TPL might be great for startup tasks. There is one 'global' task, Initialize, that has many sequences and chunks (in TPL these could be child tasks) that can have different degrees of parallelization. It also requires global 'command and control' and 'top-down' composition and error handling (a type of event correlation). If any critical part of startup fails then tear-down the service (cancel outstanding parallel tasks) and exit the service quickly. So global handling and command-control programming style (root of the call hierarchy has all control).
Once everything is initialized you want bottom-up event correlation and error handling. The function at the top of the callstack has no control (usually a WCF operation or I/O completion callback). You only want global state to be touched when absolutely necessary, not touched by ignorable infrastructure errors, and not by errors that are ignorable according to business rules.
Startup, large complex top-down composable work:
TPL
Streams, composable business logic (streams that include error/end):
RxNet
With .NET 5 the TPL dataflow libraries might change things again, but for now I much prefer RxNet for composing and routing 'messages' between layers. We recently used it in a project and when you have to correctly handle error scenarios it shines. Imagine a WCF callback for a business operation that is completely async, where business logic has to fire if the notification fails (delivery of the data to the client). The code isn't pretty in either case, but is better and can be handled at the right level of the lego blocks with RxNet.
Add a comment