C#
EndInvoke
Callback
Generics
Asynchronous Programming

c calling endinvoke in callback, using generics

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

In C#, the Asynchronous Programming Model (APM) uses BeginInvoke and EndInvoke pairs on delegates to run methods asynchronously. A callback function fires when the async operation completes, and inside that callback you call EndInvoke to retrieve the result. By combining this pattern with generics, you can build a reusable async invoker that works with any delegate return type. This article walks through the full pattern, from basics to a generic helper, and covers the pitfalls you are likely to encounter.

The APM Pattern in Brief

The APM pattern revolves around three pieces:

  1. A delegate that defines the method signature you want to call asynchronously.
  2. BeginInvoke which starts the operation on a thread pool thread and returns an IAsyncResult.
  3. EndInvoke which blocks until the operation completes and returns the result (or rethrows any exception that occurred).

Here is a minimal example without generics:

csharp
1delegate int MathOperation(int a, int b);
2
3static int Add(int a, int b)
4{
5    Thread.Sleep(1000); // simulate work
6    return a + b;
7}
8
9static void Main()
10{
11    MathOperation op = Add;
12
13    IAsyncResult asyncResult = op.BeginInvoke(3, 5, null, null);
14
15    // Block until the result is ready
16    int result = op.EndInvoke(asyncResult);
17    Console.WriteLine(result); // 8
18}

Adding a Callback

Instead of blocking on EndInvoke in the calling code, you can provide a callback method. This callback fires automatically when the async operation finishes:

csharp
1delegate int MathOperation(int a, int b);
2
3static int Multiply(int a, int b)
4{
5    Thread.Sleep(1000);
6    return a * b;
7}
8
9static void OnComplete(IAsyncResult ar)
10{
11    // Retrieve the original delegate from AsyncState
12    MathOperation op = (MathOperation)ar.AsyncState;
13
14    // Call EndInvoke to get the result
15    int result = op.EndInvoke(ar);
16    Console.WriteLine($"Result: {result}");
17}
18
19static void Main()
20{
21    MathOperation op = Multiply;
22
23    // Pass the delegate as the state object so the callback can access it
24    op.BeginInvoke(4, 7, OnComplete, op);
25
26    Console.WriteLine("Operation started, doing other work...");
27    Thread.Sleep(2000); // keep main thread alive
28}

The fourth parameter of BeginInvoke is the state object (also called AsyncState). By passing the delegate itself as the state, the callback can cast ar.AsyncState back to the delegate type and call EndInvoke.

The Problem: Type-Specific Callbacks

The callback above only works with MathOperation. If you have a different delegate type, you need a different callback. This leads to duplicated code:

csharp
1delegate string FormatOperation(string template, int value);
2
3static void OnFormatComplete(IAsyncResult ar)
4{
5    FormatOperation op = (FormatOperation)ar.AsyncState;
6    string result = op.EndInvoke(ar);
7    Console.WriteLine($"Formatted: {result}");
8}

Every delegate type needs its own callback method. Generics solve this repetition.

Generic Async Invoker

You can build a generic helper that works with Func<> delegates of any return type:

csharp
1static class AsyncHelper
2{
3    public static void InvokeAsync<TResult>(
4        Func<TResult> operation,
5        Action<TResult> onComplete,
6        Action<Exception> onError = null)
7    {
8        operation.BeginInvoke(ar =>
9        {
10            try
11            {
12                TResult result = operation.EndInvoke(ar);
13                onComplete(result);
14            }
15            catch (Exception ex)
16            {
17                if (onError != null)
18                    onError(ex);
19                else
20                    throw;
21            }
22        }, null);
23    }
24}

Usage is clean and type-safe:

csharp
1// Works with any return type
2AsyncHelper.InvokeAsync(
3    () => Add(3, 5),
4    result => Console.WriteLine($"Sum: {result}")
5);
6
7AsyncHelper.InvokeAsync(
8    () => FormatString("Item #{0}", 42),
9    result => Console.WriteLine($"Formatted: {result}")
10);

The callback no longer needs to know the delegate type. The generic parameter TResult carries the return type through the entire call chain.

Handling Delegates with Parameters

For delegates with parameters, use closures to capture them:

csharp
1int a = 10, b = 20;
2
3AsyncHelper.InvokeAsync(
4    () => Add(a, b),
5    result => Console.WriteLine($"Result: {result}"),
6    ex => Console.WriteLine($"Error: {ex.Message}")
7);

The lambda () => Add(a, b) is a Func<int> that captures the parameters, avoiding the need for multiple generic overloads.

Why You Must Call EndInvoke

Skipping EndInvoke is a common mistake. It causes two problems:

  1. Resource leak: The .NET runtime allocates internal resources (wait handles, thread pool entries) for each BeginInvoke. EndInvoke releases them. Without it, resources leak until the application exits.
  2. Lost exceptions: If the async method throws an exception, EndInvoke is the only way to observe it. Without EndInvoke, the exception is silently swallowed.
csharp
// BAD: never call BeginInvoke without eventually calling EndInvoke
operation.BeginInvoke(3, 5, null, null);
// The result and any exceptions are lost

Modern Alternatives: Task and async/await

The APM pattern (BeginInvoke/EndInvoke) is a legacy approach. In modern C# (4.0+), the Task Parallel Library and async/await provide a much cleaner API:

csharp
1// Modern equivalent of the APM pattern
2static async Task<int> AddAsync(int a, int b)
3{
4    await Task.Delay(1000); // simulate work
5    return a + b;
6}
7
8static async Task Main()
9{
10    int result = await AddAsync(3, 5);
11    Console.WriteLine(result); // 8
12}

If you are writing new code, use async/await. The APM pattern is still relevant for maintaining legacy codebases and for understanding older APIs that expose Begin/End method pairs (like Stream.BeginRead/EndRead).

Common Pitfalls

  • Forgetting to call EndInvoke: Every BeginInvoke must have a matching EndInvoke. Skipping it leaks resources and hides exceptions.
  • Casting AsyncState to the wrong type: If you pass the wrong object as the state parameter, the cast in the callback throws an InvalidCastException. Always be explicit about what you pass.
  • Thread safety in callbacks: Callbacks run on thread pool threads, not the original calling thread. Accessing shared state without synchronization causes race conditions.
  • Using APM in new code: BeginInvoke/EndInvoke on delegates is not supported in .NET Core and .NET 5+. Use Task.Run and async/await for cross-platform code.

Summary

The APM pattern uses BeginInvoke to start async work and EndInvoke in a callback to retrieve results. By wrapping this in a generic helper with Func<TResult>, you avoid writing a separate callback for every delegate type. Always call EndInvoke to prevent resource leaks and lost exceptions. For new code, prefer async/await and the Task Parallel Library. If you need to bridge legacy APM APIs into modern code, use Task.Factory.FromAsync.


Course illustration
Course illustration

All Rights Reserved.