RefactorMCP catalog

Make Method Async

Makes a method that blocks on tasks async: it awaits them instead and returns a task. Its callers get the task, and block on it where they used to get the value, so only the method itself changes shape. Convert to Async repeats this on each caller up to a boundary.

Target

The method, by symbol. There are no arguments.

Precondition

  • The method is an ordinary method with a body, block or expression, 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, local function 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, parenthesised only where precedence needs it.
  • A call of the method in the method itself or in another async method is awaited, unless it is in a lambda, local function, query or lock.
  • Every other call blocks on the task with .GetAwaiter().GetResult(), so the caller keeps its signature and still gets the value.

Preserved

  • The values computed and the order calls are made in: a caller that blocks gets the value the method used to return, once the method has finished.
  • Comments and layout around the changed calls and declaration.
  • Waits inside lambdas and local functions, which may run later, keep blocking.

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. A caller that blocks on that context's thread, as the callers left blocking do, can deadlock where the original wait did not, if the awaited task completed without needing the context.
  • Method names keep no Async suffix; Rename adds one.

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.

#across-projects

A caller in a project that references the method's project blocks on the task, and gets no using directive it does not need

success
Targetsymbol M:Stock.Inventory.Available(System.String)
ProjectsCore; App → Core
App/Report.csmodified
11 namespace Stock
22 {
33 public class Report
44 {
55 private readonly int _widgets;
66
77 public Report(Inventory inventory)
88 {
9− _widgets = inventory.Available("widget");
9+ _widgets = inventory.Available("widget").GetAwaiter().GetResult();
1010 }
1111
1212 public string Summary(Inventory inventory)
1313 {
14− return "Gadgets: " + inventory.Available("gadget") + (inventory.InStock("widget") ? "" : " (no widgets)");
14+ return "Gadgets: " + inventory.Available("gadget").GetAwaiter().GetResult() + (inventory.InStock("widget") ? "" : " (no widgets)");
1515 }
1616 }
1717 }
Core/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
2323 public bool InStock(string sku)
2424 {
25− return Available(sku) > 0;
25+ return Available(sku).GetAwaiter().GetResult() > 0;
2626 }
2727 }
2828 }

#already-async-rejected

Refuses a method that already returns a task

refusal
Targetsymbol M:Stock.Inventory.Available(System.String)
Refusesalready-async; every file is left unchanged
Inventory.csinput
1 using System.Threading.Tasks;
2
3 namespace Stock
4 {
5 public class Inventory
6 {
7 public Task<int> Available(string sku)
8 {
9 return Task.FromResult(Task.FromResult(sku.Length).Result);
10 }
11 }
12 }

#async-caller-awaits

A wait written as GetAwaiter().GetResult() is awaited, and a caller that is already async awaits the call instead of blocking

success
Targetsymbol M:Data.Loader.Load
Loader.csmodified
11 using System.Threading.Tasks;
22
33 namespace Data
44 {
55 public class Loader
66 {
77 private readonly Task<string> _pending = Task.FromResult("data");
88
9− public string Load()
9+ public async Task<string> Load()
1010 {
11− return _pending.GetAwaiter().GetResult();
11+ return await _pending;
1212 }
1313
1414 public async Task<int> LengthAsync()
1515 {
1616 await Task.Yield();
17− return Load().Length;
17+ return (await Load()).Length;
1818 }
1919 }
2020 }

#callers-block

A method reading a task's Result awaits it and returns Task<int>; every caller, in its own file or another, blocks on the task it now returns

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
2323 public bool InStock(string sku)
2424 {
25− return Available(sku) > 0;
25+ return Available(sku).GetAwaiter().GetResult() > 0;
2626 }
2727 }
2828 }
Report.csmodified
11 namespace Stock
22 {
33 public class Report
44 {
55 private readonly int _widgets;
66
77 public Report(Inventory inventory)
88 {
9− _widgets = inventory.Available("widget");
9+ _widgets = inventory.Available("widget").GetAwaiter().GetResult();
1010 }
1111
1212 public string Summary(Inventory inventory)
1313 {
14− return "Gadgets: " + inventory.Available("gadget") + (inventory.InStock("widget") ? "" : " (no widgets)");
14+ return "Gadgets: " + inventory.Available("gadget").GetAwaiter().GetResult() + (inventory.InStock("widget") ? "" : " (no widgets)");
1515 }
1616 }
1717 }

#constructor-rejected

Refuses a constructor, which cannot be async

refusal
Targetsymbol M:Stock.Inventory.#ctor
Refusesnot-a-method; every file is left unchanged
Inventory.csinput
1 using System.Threading.Tasks;
2
3 namespace Stock
4 {
5 public class Inventory
6 {
7 private readonly int _count;
8
9 public Inventory()
10 {
11 _count = Task.FromResult(3).Result;
12 }
13 }
14 }

#generic-expression-bodied

An expression-bodied generic method awaits in its expression body and returns Task<T>; a caller passing a type argument blocks

success
Targetsymbol M:Data.Settings.Load``1(System.String)
Settings.csmodified
11 using System.Threading.Tasks;
22
33 namespace Data
44 {
55 public class Cache
66 {
77 public Task<T> GetAsync<T>(string key) => Task.FromResult(default(T));
88 }
99
1010 public class Settings
1111 {
1212 private readonly Cache _cache = new Cache();
1313
14− public T Load<T>(string key) => _cache.GetAsync<T>(key).Result;
14+ public async Task<T> Load<T>(string key) => await _cache.GetAsync<T>(key);
1515
16− public int Timeout() => Load<int>("timeout") * 1000;
16+ public int Timeout() => Load<int>("timeout").GetAwaiter().GetResult() * 1000;
1717 }
1818 }

#iterator-rejected

Refuses an iterator, which returns a sequence rather than running when called

refusal
Targetsymbol M:Stock.Inventory.Available(System.String)
Refusesiterator; every file is left unchanged
Inventory.csinput
1 using System.Collections.Generic;
2 using System.Threading.Tasks;
3
4 namespace Stock
5 {
6 public class Inventory
7 {
8 public IEnumerable<int> Available(string sku)
9 {
10 yield return Task.FromResult(sku.Length).Result;
11 }
12 }
13 }

#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> Counter()
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 using System.Threading.Tasks;
2
3 namespace Stock
4 {
5 public class Inventory
6 {
7 public int Available(string sku)
8 {
9 return sku.Length;
10 }
11 }
12 }

#nullable-reference

With nullable reference types enabled, a method returning string? returns Task<string?>

success
Targetsymbol M:Data.Directory.Find(System.String)
Projectnullable enable
Directory.csmodified
11 using System.Threading.Tasks;
22
33 namespace Data
44 {
55 public class Directory
66 {
77 public Task<string?> FindAsync(string key) => Task.FromResult<string?>(null);
88
9− public string? Find(string key)
9+ public async Task<string?> Find(string key)
1010 {
11− return FindAsync(key).Result;
11+ return await FindAsync(key);
1212 }
1313
1414 public string Describe(string key)
1515 {
16− return Find(key) ?? "none";
16+ return Find(key).GetAwaiter().GetResult() ?? "none";
1717 }
1818 }
1919 }

#preserves-comments

Comments on the declaration and around the awaited call stay in place, and a wait inside a lambda, which may run later, keeps blocking

success
Targetsymbol M:Stock.Inventory.Available(System.String)
Inventory.csmodified
11 using System;
2+using System.Threading.Tasks;
23
34 namespace Stock
45 {
56 public class Inventory
67 {
78 private readonly Store _store = new Store();
89
910 // Counts what is left after the reserve.
10− public int Available(string sku)
11+ public async Task<int> Available(string sku)
1112 {
1213 // Ask the store first.
13− var count = _store.CountAsync(sku).Result; // blocks
14+ var count = await _store.CountAsync(sku); // blocks
1415 Func<int> recount = () => _store.CountAsync(sku).Result;
1516 return count - recount();
1617 }
1718 }
1819 }
Store.csunchanged
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 }

#ref-parameters-rejected

Refuses a method with an out parameter, which an async method cannot have

refusal
Targetsymbol M:Stock.Inventory.Available(System.String,System.Boolean@)
Refusesref-parameters; every file is left unchanged
Inventory.csinput
1 using System.Threading.Tasks;
2
3 namespace Stock
4 {
5 public class Inventory
6 {
7 public int Available(string sku, out bool known)
8 {
9 known = true;
10 return Task.FromResult(sku.Length).Result;
11 }
12 }
13 }

#virtual-method-rejected

Refuses a virtual method, whose signature overrides share

refusal
Targetsymbol M:Stock.Inventory.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 Task.FromResult(sku.Length).Result;
10 }
11 }
12 }

#void-method

A void method that waits on a task returns Task, and a caller that is a statement blocks on it

success
Targetsymbol M:Data.Repository.Save(System.String)
Repository.csmodified
11 using System.Threading.Tasks;
22
33 namespace Data
44 {
55 public class Connection
66 {
77 public Task WriteAsync(string row) => Task.CompletedTask;
88 }
99
1010 public class Repository
1111 {
1212 private readonly Connection _connection = new Connection();
1313
14− public void Save(string row)
14+ public async Task Save(string row)
1515 {
16− _connection.WriteAsync(row).Wait();
16+ await _connection.WriteAsync(row);
1717 }
1818
1919 public void SaveAll(string[] rows)
2020 {
2121 foreach (var row in rows)
22− Save(row);
22+ Save(row).GetAwaiter().GetResult();
2323 }
2424 }
2525 }