Cysharp / Cysharp/UniTask

Task.AsUniTask() fails to compile from engine-free asmdef consumers (CS0012 for UnityEngine.Awaitable)

Open
#716 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
11.2k
Forks
1k
PR merge metrics
No merged PRs in 30d

Description

Summary

Calling someTask.AsUniTask() from an asmdef with "noEngineReferences": true produces:

error CS0012: The type 'Awaitable' is defined in an assembly that is not referenced.
You must add a reference to assembly 'UnityEngine.CoreModule, ...'.

The receiver is System.Threading.Tasks.Task — nothing engine-related at the call site. The error comes from overload resolution pulling in the UnityAwaitableExtensions.AsUniTask(this
UnityEngine.Awaitable) candidate, even though it isn't applicable.

Environment

  • UniTask: 2.5.11
  • Unity: 6000.x (any Unity ≥ 2023.1, since Runtime/UnityAwaitableExtensions.cs is gated on UNITY_2023_1_OR_NEWER)

Repro

Consumer asmdef:
{ "name": "MyApp.Logic", "references": ["UniTask"], "noEngineReferences": true }

Call site:
using Cysharp.Threading.Tasks;
using System.Threading.Tasks;

public class Foo
{
public void Run(Task t) => t.AsUniTask().Forget();
}

→ CS0012.

Diagnosis

Runtime/UnityAwaitableExtensions.cs declares its extensions in the same Cysharp.Threading.Tasks namespace as UniTaskExtensions.AsUniTask(this Task). To check applicability of every
AsUniTask candidate in scope, the compiler must resolve every candidate's parameter types — including UnityEngine.Awaitable, which the consumer's asmdef can't see. A single optional,
engine-specific bridge therefore contaminates overload resolution for every AsUniTask call.

Why it matters

Clean-architecture Unity projects often mark Application/Domain layers as engine-free so business logic stays unit-testable outside the player. Those layers regularly need to bridge
third-party Task-returning APIs (network SDKs, etc.) into UniTask. Today, every such bridge forces noEngineReferences: false, dropping the layering guarantee.

Concrete case I hit: an engine-free Application layer calling a third-party SDK whose async API returns Task; .AsUniTask().Forget() fails to compile despite the call never touching
Awaitable.

Prior reports of the same packaging pattern

This isn't a one-off — the same root cause (monolithic UniTask assembly + extensions referencing optional engine surface) has surfaced repeatedly:

  • #222 — UniTask fails to compile when Physics / Physics2D modules are disabled
  • #35 — Request to separate editor scripts into their own assembly
  • #572 — Pain consuming UniTask.NetCore from Unity
  • #416 — Originating issue for UnityAwaitableExtensions, no discussion of the engine-free-consumer side effect at the time

The structural fix is the same in each case: split optional surface into its own asmdef so consumers can opt in.

Proposed fixes

A — sub-namespace the Awaitable extensions

Move UnityAwaitableExtensions from Cysharp.Threading.Tasks to Cysharp.Threading.Tasks.UnityEngine (or similar). Consumers' existing using Cysharp.Threading.Tasks; stops bringing the
Awaitable methods into extension-method scope; engine-aware code adds using Cysharp.Threading.Tasks.UnityEngine;. One-line change. Breaking for users who relied on the bridge being in the
root namespace; documentable.

B — split into its own asmdef

Mirror what UniTask.Linq already does: create UniTask.UnityAwaitable.asmdef containing only UnityAwaitableExtensions.cs, gated on UNITY_2023_1_OR_NEWER, referencing UniTask. Engine-free
consumers don't reference it; engine-aware consumers do. Structurally cleaner, more consistent with the existing UniTask.Linq partition. Breaking for asmdef references; one-line migration
per consuming asmdef.

Workarounds available today

  • Static-method form: UniTaskExtensions.AsUniTask(task).Forget(); — pins resolution to one class.
  • Wrap in an async UniTaskVoid helper that awaits the Task directly (no .AsUniTask() call).
  • Flip noEngineReferences: false (loses the layering guarantee).

Happy to PR either fix if there's a preferred direction.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Runtime/UnityAwaitableExtensions.cs and the existing UniTask.Linq assembly definition, then reproduce the CS0012 case using the noEngineReferences asmdef and Task.AsUniTask() call described here. Compare the proposed namespace and separate-asmdef approaches, and confirm done when an engine-free consumer compiles without UnityEngine.CoreModule while engine-aware Awaitable usage remains available.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, unity
Domain
build-system
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.