Асинхронно использовать WebClient в ASP.NET MVC?

У меня есть приложение ASP.NET MVC, которое в настоящее время использует класс WebClient для простого вызова внешней веб-службы из действия контроллера.

В настоящее время я использую метод DownloadString, который выполняется синхронно. Я столкнулся с проблемами, когда внешняя веб-служба не отвечает, что приводит к тому, что все мое приложение ASP.NET не отвечает ни на какие потоки.

Каков наилучший способ решить эту проблему? Существует метод DownloadStringAsync, но я не знаю, как его вызвать из контроллера. Нужно ли использовать класс AsyncController? Если да, то как взаимодействуют метод AsyncController и DownloadStringAsync?

Спасибо за помощь.


person Brian Vallelunga    schedule 08.08.2009    source источник


Ответы (2)


Я думаю, что использование AsyncControllers поможет вам здесь, поскольку они разгружают обработку из потока запросов.

Я бы использовал что-то вроде этого (используя шаблон события, как описано в эта статья):

public class MyAsyncController : AsyncController
{
    // The async framework will call this first when it matches the route
    public void MyAction()
    {
        // Set a default value for our result param
        // (will be passed to the MyActionCompleted method below)
        AsyncManager.Parameters["webClientResult"] = "error";
        // Indicate that we're performing an operation we want to offload
        AsyncManager.OutstandingOperations.Increment();

        var client = new WebClient();
        client.DownloadStringCompleted += (s, e) =>
        {
            if (!e.Cancelled && e.Error == null)
            {
                // We were successful, set the result
                AsyncManager.Parameters["webClientResult"] = e.Result;
            }
            // Indicate that we've completed the offloaded operation
            AsyncManager.OutstandingOperations.Decrement();
        };
        // Actually start the download
        client.DownloadStringAsync(new Uri("http://www.apple.com"));
    }

    // This will be called when the outstanding operation(s) have completed
    public ActionResult MyActionCompleted(string webClientResult)
    {
        ViewData["result"] = webClientResult;
        return View();
    }
}

И убедитесь, что вы настроили любые маршруты, которые вам нужны, например (в Global.asax.cs):

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapAsyncRoute(
            "Default",
            "{controller}/{action}/{id}",
            new { controller = "Home", action = "Index", id = "" }
        );
    }
}
person Michael Hart    schedule 09.08.2009
comment
Есть ли способ добавить тайм-аут к асинхронному вызову веб-клиента? - person THX-1138; 19.04.2011
comment
Просто примечание: кажется, этот ответ был опубликован в 09 году еще в дни MVC 1.0. Теперь с MVC 2/3 ответ НЕМНОГО отличается. Метод MapAsyncRoute больше не нужен. Кроме того, метод MyAction теперь необходимо переименовать в MyActionAsync. В остальном все работает так же. - person BFree; 17.06.2011

Метод DownloadStringAsync использует модель событий, вызывая DownloadStringCompleted по завершении. Вы также можете остановить запрос, если он выполняется слишком долго, вызвав WebClient.CancelAsync(). Это позволит вашему основному потоку запроса и вашему потоку WebClient работать параллельно и позволит вам точно решить, как долго вы хотите, чтобы ваш основной поток ждал перед возвратом.

В приведенном ниже примере мы инициируем загрузку и устанавливаем обработчик событий, который мы хотим вызвать после ее завершения. DownloadStringAsync возвращается немедленно, поэтому мы можем продолжить обработку оставшейся части нашего запроса.

Чтобы продемонстрировать более детальный контроль над этой операцией, когда мы достигнем конца действия нашего контроллера, мы можем проверить, завершена ли загрузка; если нет, дайте ему еще 3 секунды, а затем прервите.

string downloadString = null;

ActionResult MyAction()
{
    //get the download location
    WebClient client = StartDownload(uri);
    //do other stuff
    CheckAndFinalizeDownload(client);
    client.Dispose();
}

WebClient StartDownload(Uri uri)
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(Download_Completed);
    client.DownloadStringAsync(uri);
    return client;
}

void CheckAndFinalizeDownload(WebClient client)
{
    if(this.downloadString == null)
    {
        Thread.Sleep(3000);
    }
    if(this.downloadString == null)
    {
        client.CancelAsync();
        this.downloadString = string.Empty;
    }
}

void Download_Completed(object sender, DownloadStringCompletedEventArgs e)
{
    if(!e.Cancelled && e.Error == null)
    {
        this.downloadString = (string)e.Result;
    }
}
person Rex M    schedule 08.08.2009
comment
Я считаю, что этот подход по-прежнему будет блокировать поток запросов - вы все еще находитесь в потоке запросов во время вызова Sleep. Здесь помогут асинхронные контроллеры, поскольку они освобождают поток запросов, пока вы ожидаете внешнего ресурса. - person Michael Hart; 09.08.2009
comment
@ Майкл действительно. Этот подход будет выполняться параллельно с потоком запроса, если только запрос WebClient не будет завершен к тому времени, когда поток контроллера сделает все остальное, что ему нужно сделать, - в этот момент контроллер выбирает, следует ли блокировать/спать или продолжать. У меня не сложилось впечатление, что вопрос конкретно в том, как использовать AsyncController, а скорее в том, как заставить WebClient работать асинхронно. Возможно, это неправильное прочтение вопроса. - person Rex M; 09.08.2009
comment
@Rex Да, я думаю, судя по описанию веб-приложения, которое испытывает нехватку потоков и не отвечает, вероятно, у OP заканчиваются потоки запросов. Ограничение внешнего ответа до 3 секунд немного поможет (если ваши внешние запросы ранее занимали больше времени), но это все еще довольно долгое время для удержания потока, и, следовательно, он не будет масштабироваться. - person Michael Hart; 09.08.2009