C# Async/await with a Func delegate

By FoxLearn 1/21/2025 8:31:17 AM   14
To make a Func delegate awaitable, you need to return a Task from the delegate.

Here’s an example of a simple Func that accepts a parameter and returns a Task:

Func<int, Task> delayMessage = async (seconds) =>
{
    await Task.Delay(1000 * seconds);
    Console.WriteLine($"Waited for {seconds} seconds!");
};

In this example, the Func accepts an int parameter and returns a Task. The method waits for the specified number of seconds before printing a message. Since the return type is a Task, it can be awaited.

await delayMessage(5);

In this case, after 5 seconds, you will see the output: Waited for 5 seconds!

Notice that this delegate doesn't return a value. You would typically use an Action delegate if no value needs to be returned, but since Action cannot be awaited, we use Func with Task as the return type to make it awaitable.

Awaitable Func Delegate Returning a Value

To create a Func delegate that is awaitable and returns a value, you use Task<T> as the return type.

Here’s an example where we calculate the square of a number:

Func<int, Task<int>> calculateSquare = async (num) =>
{
    await Task.Delay(100); // Simulating async operation
    return num * num;
};

In this example, the Func takes an int parameter and returns a Task<int>. The result of awaiting this Func is an integer value, which is the square of the input number.

int result = await calculateSquare(4);

After awaiting, the result will be 16.

Using an Awaitable Func Delegate in a Strategy Pattern

Imagine you need to fetch records asynchronously from a database, serialize the data to JSON, and save it to a file. However, the way records are fetched differs for each type of data (such as Product, Customer, etc.).

In this case, you can use a delegate to handle the fetching logic, ensuring the fetching is done asynchronously using an awaitable Func delegate.

Here’s how you can define an async method that accepts an awaitable Func delegate:

private async Task SaveToFile<TRecord>(string filePath, string id, Func<string, Task<TRecord>> fetchData)
{
    var record = await fetchData(id);
    var json = JsonSerializer.Serialize(record);
    File.WriteAllText(filePath, json);
}

This method takes a Func delegate as a parameter, which accepts a string (the id of the record to fetch) and returns a Task<TRecord> (a generic type TRecord). When awaited, it will return a record of the specified type, such as a Product or Customer.

For example, if you're working with an Employee record, you would call the method like this:

await SaveToFile<Employee>(@"C:\temp\employee.json", "12345", async (id) => await EmployeeRepo.Get(id));

In this call, the EmployeeRepo.Get(id) method fetches the Employee record by its ID, and the record is then serialized to JSON and saved to the specified file path.