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:
- A delegate that defines the method signature you want to call asynchronously.
BeginInvokewhich starts the operation on a thread pool thread and returns anIAsyncResult.EndInvokewhich blocks until the operation completes and returns the result (or rethrows any exception that occurred).
Here is a minimal example without generics:
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:
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:
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:
Usage is clean and type-safe:
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:
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:
- Resource leak: The .NET runtime allocates internal resources (wait handles, thread pool entries) for each
BeginInvoke.EndInvokereleases them. Without it, resources leak until the application exits. - Lost exceptions: If the async method throws an exception,
EndInvokeis the only way to observe it. WithoutEndInvoke, the exception is silently swallowed.
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:
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
BeginInvokemust have a matchingEndInvoke. 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/EndInvokeon delegates is not supported in .NET Core and .NET 5+. UseTask.Runandasync/awaitfor 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.

