[Canonical page](<https://docs.dhtmlx.com/planning-engine/net/how-to/call-from-async-code/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Call from async code

Run a long scheduling call from an ASP.NET Core handler, a UI event or any other `async` method without blocking the calling thread.

Every [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) method is synchronous and CPU-bound. There are no `Async` overloads and no `CancellationToken` parameters. Move the heavy calls ([`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>), [`Evaluate`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md>), [`SuggestAssignments`](<https://docs.dhtmlx.com/planning-engine/net/api/project-suggest-assignments.md>) on large projects) to the thread pool with `Task.Run`.

## Prerequisites

- A solved [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>): [`Solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) has run.
- Only one thread uses that `Project` at a time. A `Project` isn't thread-safe. If several requests reach the same instance, see [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/net/how-to/share-a-project-between-threads.md>).

## Steps

1. Clone the project on the calling thread. The background call then works on its own copy, and the original stays usable while the call runs. [`Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>) is cheap, because the copies share their data until one of them is edited:

   ```csharp
   var draft = project.Clone();
   ```

2. Run the call on the thread pool and await it. Exceptions such as [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) and [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) surface at the `await`:

   ```csharp
   var leveled = await Task.Run(() => draft.Level(new LevelingOptions()));
   ```

3. To stop waiting after a timeout or when the client disconnects, pass a token to `Task.Run` and to `WaitAsync`:

   ```csharp
   using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
   var leveled = await Task.Run(() => draft.Level(new LevelingOptions()), cts.Token)
       .WaitAsync(cts.Token);
   ```

   The token can't stop a call that has already started. `WaitAsync` throws `OperationCanceledException` and returns control to you, but the call keeps running on the pool thread until it finishes. That's why the call runs on `draft`. When you stop waiting, drop the clone. The original stays untouched.

4. Commit the result. [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) has already accepted the leveled state on `draft`, so replace the original with it:

   ```csharp
   project = draft;
   ```

   Replace the original only if nobody edited it while the call ran. Otherwise the result comes from an older state, so run the call again on a new clone.

   [`Evaluate`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md>) and [`SuggestAssignments`](<https://docs.dhtmlx.com/planning-engine/net/api/project-suggest-assignments.md>) never modify the project, so there's nothing to swap. Pass the change set you pick to [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>) on the original.

The full example levels two tasks in the background and gives up after 30 seconds:

```csharp
using Plen;

// Monday, August 10, 2026, on the default calendar.
var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = monday, Duration = 2.0 },
        new TaskData("B") { StartDate = monday, Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments =
    [
        new AssignmentData { Task = "A", Resource = "dev1", Value = 1.0 },
        new AssignmentData { Task = "B", Resource = "dev1", Value = 1.0 },
    ],
};

var project = new Project();
project.Solve(data, new SolveOptions());

var draft = project.Clone();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
    var leveled = await Task.Run(() => draft.Level(new LevelingOptions()), cts.Token)
        .WaitAsync(cts.Token);
    project = draft;
    foreach (var update in leveled.Schedule.Updates)
    {
        Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}, delay {update.LevelingDelay}");
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("leveling timed out; the project is unchanged");
}
```

## Score candidates on several cores

[`EvaluateWith`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md#evaluatewith>) with [`Execution.Parallel()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/evaluate/execution.md#parallel>) spreads candidates over worker threads, but it still blocks the caller until every candidate is scored. Wrap it in `Task.Run` too:

```csharp
var copy = project.Clone();
var scores = await Task.Run(() =>
    copy.EvaluateWith(candidates, new EvaluateOptions(), Execution.Parallel()));
```

`Execution.Parallel()` uses `Environment.ProcessorCount` threads. On a shared web server, pass a smaller count, such as `Execution.Parallel(2)`, so one request doesn't take every core.

## Work that outlives the request

`Task.Run` still keeps the request open until the call returns. For a call that takes longer than a client should wait, queue the work to a hosted background worker (`BackgroundService`), return right away, and let the client fetch the result later. Follow the same rules in the worker: give it its own [`Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>), made on the thread that owns the project.

## Result

The example prints the task that leveling moved. The thread that awaited it stays free while [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) runs:

```text
B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2
```

If leveling takes longer than 30 seconds, the example prints `leveling timed out; the project is unchanged` and `project` keeps its unleveled baseline. For the leveling flow itself, see [Preview leveling](<https://docs.dhtmlx.com/planning-engine/net/how-to/preview-leveling.md>). For the exceptions each call throws, see [Error handling](<https://docs.dhtmlx.com/planning-engine/net/error-handling.md>).
