async/await
webmethod
asmx
web services
C#

Is it possible to use async/await in webmethod asmx service

Master System Design with Codemia

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

Introduction

ASMX web services do not natively support async/await because the ASMX pipeline was designed for synchronous request handling in .NET Framework 1.0/2.0. While you can use async/await inside a [WebMethod] body for internal operations, the ASMX infrastructure does not properly await the result, leading to unpredictable behavior. The recommended path is to migrate to WCF (which supports async operations) or ASP.NET Web API / ASP.NET Core (which fully support async/await). For legacy code that must stay on ASMX, use the older APM pattern (Begin/End) or call .Result on async tasks synchronously.

The Problem

csharp
1// This DOES NOT work correctly in ASMX
2[WebService(Namespace = "http://tempuri.org/")]
3public class MyService : WebService
4{
5    [WebMethod]
6    public async Task<string> GetDataAsync()  // WRONG — ASMX ignores Task return
7    {
8        var result = await SomeAsyncOperation();
9        return result;
10    }
11}

ASMX expects [WebMethod] to return the result directly (e.g., string, int, DataSet). It does not understand Task<T> as a return type. The serializer tries to serialize the Task object itself, returning garbage XML or throwing an exception.

Workaround 1: Synchronous Wrapper

Block on the async call using .Result or .GetAwaiter().GetResult():

csharp
1[WebService(Namespace = "http://tempuri.org/")]
2public class MyService : WebService
3{
4    [WebMethod]
5    public string GetData()
6    {
7        // Block synchronously on the async operation
8        var result = GetDataInternalAsync().GetAwaiter().GetResult();
9        return result;
10    }
11
12    private async Task<string> GetDataInternalAsync()
13    {
14        using var client = new HttpClient();
15        var response = await client.GetStringAsync("https://api.example.com/data");
16        return response;
17    }
18}

This works but defeats the purpose of async — the thread is blocked while waiting. Risk of deadlock exists if the async code captures the synchronization context.

Avoiding Deadlocks

csharp
1[WebMethod]
2public string GetData()
3{
4    // ConfigureAwait(false) prevents deadlock by not capturing the sync context
5    var result = GetDataInternalAsync().ConfigureAwait(false).GetAwaiter().GetResult();
6    return result;
7}
8
9// Or use Task.Run to escape the synchronization context
10[WebMethod]
11public string GetDataSafe()
12{
13    var result = Task.Run(() => GetDataInternalAsync()).GetAwaiter().GetResult();
14    return result;
15}

Workaround 2: APM Pattern (Begin/End)

ASMX supports the older Asynchronous Programming Model (APM) with Begin/End method pairs:

csharp
1[WebService(Namespace = "http://tempuri.org/")]
2public class MyService : WebService
3{
4    // Synchronous version (required for WSDL generation)
5    [WebMethod]
6    public string GetData()
7    {
8        return GetDataInternalAsync().GetAwaiter().GetResult();
9    }
10
11    // APM Begin method
12    [WebMethod]
13    public IAsyncResult BeginGetData(AsyncCallback callback, object state)
14    {
15        var task = GetDataInternalAsync();
16        var tcs = new TaskCompletionSource<string>(state);
17
18        task.ContinueWith(t =>
19        {
20            if (t.IsFaulted) tcs.SetException(t.Exception.InnerExceptions);
21            else if (t.IsCanceled) tcs.SetCanceled();
22            else tcs.SetResult(t.Result);
23            callback?.Invoke(tcs.Task);
24        });
25
26        return tcs.Task;
27    }
28
29    // APM End method
30    public string EndGetData(IAsyncResult result)
31    {
32        return ((Task<string>)result).Result;
33    }
34
35    private async Task<string> GetDataInternalAsync()
36    {
37        using var client = new HttpClient();
38        return await client.GetStringAsync("https://api.example.com/data")
39            .ConfigureAwait(false);
40    }
41}

Migration to ASP.NET Web API

The proper solution is to migrate from ASMX to a modern framework:

csharp
1// ASP.NET Web API (full async support)
2public class DataController : ApiController
3{
4    [HttpGet]
5    [Route("api/data")]
6    public async Task<IHttpActionResult> GetData()
7    {
8        using var client = new HttpClient();
9        var result = await client.GetStringAsync("https://api.example.com/data");
10        return Ok(result);
11    }
12}
csharp
1// ASP.NET Core (recommended for new projects)
2[ApiController]
3[Route("api/[controller]")]
4public class DataController : ControllerBase
5{
6    private readonly HttpClient _client;
7
8    public DataController(IHttpClientFactory factory)
9    {
10        _client = factory.CreateClient();
11    }
12
13    [HttpGet]
14    public async Task<IActionResult> GetData()
15    {
16        var result = await _client.GetStringAsync("https://api.example.com/data");
17        return Ok(result);
18    }
19}

Side-by-Side Migration Strategy

Run ASMX and Web API simultaneously during migration:

csharp
1// In Global.asax or Startup
2public class WebApiApplication : HttpApplication
3{
4    protected void Application_Start()
5    {
6        // Register Web API routes alongside existing ASMX
7        GlobalConfiguration.Configure(WebApiConfig.Register);
8    }
9}
10
11// WebApiConfig.cs
12public static class WebApiConfig
13{
14    public static void Register(HttpConfiguration config)
15    {
16        config.MapHttpAttributeRoutes();
17        config.Routes.MapHttpRoute(
18            name: "DefaultApi",
19            routeTemplate: "api/{controller}/{id}",
20            defaults: new { id = RouteParameter.Optional }
21        );
22    }
23}

Existing ASMX endpoints (.asmx) continue to work while new endpoints use Web API (/api/...). Migrate clients one at a time.

WCF Alternative

WCF supports Task-based async natively:

csharp
1[ServiceContract]
2public interface IDataService
3{
4    [OperationContract]
5    Task<string> GetDataAsync();
6}
7
8public class DataService : IDataService
9{
10    public async Task<string> GetDataAsync()
11    {
12        using var client = new HttpClient();
13        return await client.GetStringAsync("https://api.example.com/data");
14    }
15}

Common Pitfalls

  • Returning Task<T> from a [WebMethod]: ASMX does not understand Task<T> as a return type. It serializes the Task object itself instead of awaiting its result, producing incorrect XML responses.
  • Deadlocking with .Result in ASMX: Calling .Result on a task that captures the SynchronizationContext deadlocks because the awaiter needs the ASMX thread, which is blocked waiting for .Result. Use .ConfigureAwait(false) or Task.Run() to avoid this.
  • Assuming ASMX benefits from async: Even with workarounds, ASMX cannot release the request thread during async operations like Web API can. The thread pool benefit of async is lost when using .Result or .GetAwaiter().GetResult().
  • Ignoring ASMX deprecation: ASMX is a legacy technology with no active development. Microsoft recommends migrating to ASP.NET Core or at minimum ASP.NET Web API. New features and security patches are not added to ASMX.
  • Not testing async workarounds under load: Synchronous wrappers around async code can exhaust the thread pool under high concurrency. Load test any ASMX async workarounds to ensure they do not cause thread starvation.

Summary

  • ASMX [WebMethod] does not support async Task<T> return types — the pipeline predates async/await
  • Use .GetAwaiter().GetResult() with .ConfigureAwait(false) as a workaround for calling async code from ASMX
  • The APM pattern (Begin/End methods) is the officially supported async pattern for ASMX
  • Migrate to ASP.NET Web API or ASP.NET Core for proper async/await support
  • Run ASMX and Web API side-by-side during migration to avoid big-bang rewrites

Course illustration
Course illustration

All Rights Reserved.