RefactorMCP catalog

Convert to Async

Makes a method that blocks on tasks async: it awaits them instead and returns a task. Its callers await it and become async in turn, repeating up to callers that cannot be async, which go on blocking on the task.

Recipe

  1. make-method-async on the method. It awaits the tasks the method blocked on and returns a task; every caller blocks on that task with .GetAwaiter().GetResult().
  2. make-method-async on each caller that can be async, which now blocks on the task, so it awaits it and its own callers block in turn. Repeat up to callers that cannot be async, such as constructors, which keep blocking.
"steps": [
  { "refactoring": "make-method-async", "target": { "symbol": "M:Stock.Inventory.Available(System.String)" } },
  { "refactoring": "make-method-async", "target": { "symbol": "M:Stock.Inventory.InStock(System.String)" } },
  { "refactoring": "make-method-async", "target": { "symbol": "M:Stock.Report.Summary(Stock.Inventory)" } }
]

The plan's first step, Change Return Type to Task, cannot run on its own: the method's body still returns the value, not a task. Make Method Async changes the return type, adds async and awaits the waits as one step, and leaves the callers blocking so each step compiles and preserves behaviour. The dedicated implementation finds the callers that can be async itself and converts them all as one change.

Arguments

None. The target is the method, by symbol.

Precondition

  • The method is an ordinary method with a body that does not already return a task, is not an iterator, does not return by reference and has no ref, out or in parameters.
  • It is not virtual, abstract, an override or an interface implementation, whose signature it shares with other methods.
  • It blocks on at least one task outside any lambda or lock: task.Result, task.Wait() or task.GetAwaiter().GetResult().
  • It is only called, never used as a method group, where a method returning a task would no longer fit the delegate.
  • The result compiles.

Transformation

  • The method is marked async and returns Task<T> in place of T, or Task in place of void. A using System.Threading.Tasks; directive is added where the file needs one.
  • Each blocking wait in the method becomes await on the task.
  • Each call of a method made async is awaited when it is in a method that can be async: one that meets the precondition above, apart from blocking on a task, or one that is already async. That method is made async the same way, and its callers in turn.
  • A call in a constructor, accessor, lambda, local function, query or lock, or in a method that cannot be async, blocks with .GetAwaiter().GetResult().

Preserved

  • The values computed and the order calls are made in.
  • Comments and layout around the changed calls and declarations.

Limitations

  • Awaiting rethrows a task's exception as it is, where .Result and .Wait() wrapped it in an AggregateException; a caller catching AggregateException sees a different exception.
  • Awaiting resumes on the captured synchronization context, which can change which thread code after the await runs on.
  • Method names keep no Async suffix; Rename adds one.
  • A caller whose signature is shared, such as an override, blocks rather than changing its whole hierarchy.

Error codes

CodeMeaning
nothing-to-awaitthe method does not block on a task
already-asyncthe method already returns a task
not-a-methodthe target is not an ordinary method
polymorphic-methodthe method is virtual, an override or an interface implementation
ref-parametersthe method has ref, out or in parameters
iteratorthe method is an iterator
method-group-referencethe method is used as a method group

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#callers-up-to-a-constructor

A method reading a task's Result awaits it and returns a task; callers in methods await it and become async in turn, and a constructor, which cannot be async, blocks on it

success
Targetsymbol M:Stock.Inventory.Available(System.String)
Inventory.csmodified
11 using System.Threading.Tasks;
22
33 namespace Stock
44 {
55 public class Store
66 {
77 public Task<int> CountAsync(string sku)
88 {
99 return Task.FromResult(sku.Length);
1010 }
1111 }
1212
1313 public class Inventory
1414 {
1515 private readonly Store _store = new Store();
1616
17− public int Available(string sku)
17+ public async Task<int> Available(string sku)
1818 {
19− var count = _store.CountAsync(sku).Result;
19+ var count = await _store.CountAsync(sku);
2020 return count > 0 ? count - 1 : 0;
2121 }
2222
23− public bool InStock(string sku)
23+ public async Task<bool> InStock(string sku)
2424 {
25− return Available(sku) > 0;
25+ return await Available(sku) > 0;
2626 }
2727 }
2828 }
Report.csmodified
1+using System.Threading.Tasks;
2+
13 namespace Stock
24 {
35 public class Report
46 {
57 private readonly int _widgets;
68
79 public Report(Inventory inventory)
810 {
9− _widgets = inventory.Available("widget");
11+ _widgets = inventory.Available("widget").GetAwaiter().GetResult();
1012 }
1113
12− public string Summary(Inventory inventory)
14+ public async Task<string> Summary(Inventory inventory)
1315 {
14− return "Gadgets: " + inventory.Available("gadget") + (inventory.InStock("widget") ? "" : " (no widgets)");
16+ return "Gadgets: " + await inventory.Available("gadget") + (await inventory.InStock("widget") ? "" : " (no widgets)");
1517 }
1618 }
1719 }

#method-group-rejected

Refuses a method used as a method group, where a method returning a task would no longer fit the delegate

refusal
Targetsymbol M:Stock.Inventory.Available(System.String)
Refusesmethod-group-reference; every file is left unchanged
Inventory.csinput
1 using System;
2 using System.Threading.Tasks;
3
4 namespace Stock
5 {
6 public class Inventory
7 {
8 public int Available(string sku)
9 {
10 return Task.FromResult(sku.Length).Result;
11 }
12
13 public Func<string, int> Lookup()
14 {
15 return Available;
16 }
17 }
18 }

#nothing-to-await-rejected

Refuses a method that blocks on no task, which would become an async method with nothing to await

refusal
Targetsymbol M:Stock.Inventory.Available(System.String)
Refusesnothing-to-await; every file is left unchanged
Inventory.csinput
1 namespace Stock
2 {
3 public class Inventory
4 {
5 public int Available(string sku)
6 {
7 return sku.Length;
8 }
9 }
10 }

#override-rejected

Refuses an override, whose signature it shares with the method it overrides

refusal
Targetsymbol M:Stock.CachedInventory.Available(System.String)
Refusespolymorphic-method; every file is left unchanged
Inventory.csinput
1 using System.Threading.Tasks;
2
3 namespace Stock
4 {
5 public class Inventory
6 {
7 public virtual int Available(string sku)
8 {
9 return sku.Length;
10 }
11 }
12
13 public class CachedInventory : Inventory
14 {
15 public override int Available(string sku)
16 {
17 return Task.FromResult(sku.Length).Result;
18 }
19 }
20 }

#recipe

Runs the recipe: Make Method Async on the method, which leaves its callers blocking on the task, then on each caller that can be async, up to a constructor, which keeps blocking

successrecipe
Recipe
  1. make-method-async on symbol M:Stock.Inventory.Available(System.String)
  2. make-method-async on symbol M:Stock.Inventory.InStock(System.String)
  3. make-method-async on symbol M:Stock.Report.Summary(Stock.Inventory)
Inventory.csmodified
11 using System.Threading.Tasks;
22
33 namespace Stock
44 {
55 public class Store
66 {
77 public Task<int> CountAsync(string sku)
88 {
99 return Task.FromResult(sku.Length);
1010 }
1111 }
1212
1313 public class Inventory
1414 {
1515 private readonly Store _store = new Store();
1616
17− public int Available(string sku)
17+ public async Task<int> Available(string sku)
1818 {
19− var count = _store.CountAsync(sku).Result;
19+ var count = await _store.CountAsync(sku);
2020 return count > 0 ? count - 1 : 0;
2121 }
2222
23− public bool InStock(string sku)
23+ public async Task<bool> InStock(string sku)
2424 {
25− return Available(sku) > 0;
25+ return await Available(sku) > 0;
2626 }
2727 }
2828 }
Report.csmodified
1+using System.Threading.Tasks;
2+
13 namespace Stock
24 {
35 public class Report
46 {
57 private readonly int _widgets;
68
79 public Report(Inventory inventory)
810 {
9− _widgets = inventory.Available("widget");
11+ _widgets = inventory.Available("widget").GetAwaiter().GetResult();
1012 }
1113
12− public string Summary(Inventory inventory)
14+ public async Task<string> Summary(Inventory inventory)
1315 {
14− return "Gadgets: " + inventory.Available("gadget") + (inventory.InStock("widget") ? "" : " (no widgets)");
16+ return "Gadgets: " + await inventory.Available("gadget") + (await inventory.InStock("widget") ? "" : " (no widgets)");
1517 }
1618 }
1719 }

#void-method

A void method that waits on a task returns a Task; a caller that is a statement awaits it, and a call in a lambda, which would change the delegate, blocks

success
Targetsymbol M:Data.Repository.Save
Repository.csmodified
11 using System;
22 using System.Threading.Tasks;
33
44 namespace Data
55 {
66 public class Database
77 {
88 public Task SaveAsync()
99 {
1010 return Task.CompletedTask;
1111 }
1212 }
1313
1414 public class Repository
1515 {
1616 private readonly Database _database = new Database();
1717
18− public void Save()
18+ public async Task Save()
1919 {
2020 // Flush before anything else reads.
21− _database.SaveAsync().Wait();
21+ await _database.SaveAsync();
2222 }
2323
24− public void SaveTwice()
24+ public async Task SaveTwice()
2525 {
26− Save();
27− Save();
26+ await Save();
27+ await Save();
2828 }
2929
3030 public Action Later()
3131 {
32− return () => Save();
32+ return () => Save().GetAwaiter().GetResult();
3333 }
3434 }
3535 }