Id

int Id { get; }

I’ve talked a bit about task identifiers before, so I’ll just cover the high points here.

First, in spite of what the documentation says, the identifiers are not actually unique. They are pretty close, but not actually unique. The identifiers are generated on-demand, and will never be zero.

The task identifiers are useful if you’re reading the ETW events or debugging with the Tasks window, but they don’t really have a use case outside of diagnostics and debugging.

Sometimes developers try to use the task identifiers as keys in a collection, to associate “extra data” with a task. This is an incorrect approach; usually what they’re looking for is an async local.

CurrentId

static int? CurrentId { get; }

The CurrentId property returns the identifier of the currently-executing task, or null if no task is executing. The key word here is executing - CurrentId only works for Delegate Tasks, not Promise Tasks.

In particular, the task returned by an async method is a Promise Task; it logically represents the async method, but it is not a Delegate Task, and does not actually have the asynchronous code as its delegate. CurrentId may or may not be null within an async method, depending on the implementation details of the underlying SynchronizationContext or TaskScheduler.

For more information, including sample code, see my post on CurrentId in async methods.

In parallel code, it is possible to use the current task identifier as a key into a collection to store task-local values or results, but that is a poor approach IMO. It’s usually far better to use the PLINQ/Parallel built-in local value and result aggregation support.