System.AggregateException

Represents one or more errors that occur during application execution.

Minimum version: >= 4.0 >= Core 1.0

Statistics

13
elmah.io logo 18

How to handle it

try
{

}
catch (System.AggregateException e)
{

}
try
{

}
catch (System.AggregateException e) when (e.Message.Contains("something"))
{

}
try
{

}
catch (System.AggregateException e) when (LogException(e))
{

}

private static bool LogException(Exception e)
{
    logger.LogError(...);
    return false;
}

How to avoid it

We haven't written anything about avoiding this exception yet. Got a good tip on how to avoid throwing System.AggregateException? Feel free to reach out through the support widget in the lower right corner with your suggestions.

Links

YouTube videos

Possible fixes from StackOverflow

Instead of implementing retry functionality that wraps the HttpClient, consider constructing the HttpClient with a HttpMessageHandler that performs the retry logic internally. For example:

public class RetryHandler : DelegatingHandler
{
    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    { }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            response = await base.SendAsync(request, cancellationToken);
            if (response.IsSuccessStatusCode) {
                return response;
            }
        }

        return response;
    }
}

public class BusinessLogic
{
    public void FetchSomeThingsSynchronously()
    {
        // ...

        // Consider abstracting this construction to a factory or IoC container
        using (var client = new HttpClient(new RetryHandler(new HttpClientHandler())))
        {
            myResult = client.PostAsync(yourUri, yourHttpContent).Result;
        }

        // ...
    }
}

ASP.NET Core 2.1 Answer

ASP.NET Core 2.1 added support for Polly directly. Here UnreliableEndpointCallerService is a class which accepts a HttpClient in its constructor. Failed requests will retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one:

services
    .AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(
        x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt))));

Also, consider reading my blog post "Optimally Configuring HttpClientFactory".

Other Platforms Answer

This implementation uses Polly to retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one. It also retries if a HttpRequestException or TaskCanceledException is thrown due to a timeout. Polly is much easier to use than Topaz.

public class HttpRetryMessageHandler : DelegatingHandler
{
    public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) {}

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
            .ExecuteAsync(() => base.SendAsync(request, cancellationToken));
}

using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler())))
{
    var result = await client.GetAsync("http://example.com");
}

As the message says, you have a task which threw an unhandled exception.

Turn on Break on All Exceptions (Debug, Exceptions) and rerun the program.
This will show you the original exception when it was thrown in the first place.


(comment appended): In VS2015 (or above). Select Debug > Options > Debugging > General and unselect the "Enable Just My Code" option.

It is, it's all about understanding the lifetimes of the various elements in play and getting those set correctly.

By default the DbContextFactory created by the AddDbContextFactory() extension method has a Singleton lifespan. If you use the AddDbContext() extension method with it's default settings it will create a DbContextOptions with a Scoped lifespan (see the source-code here), and as a Singleton can't use something with a shorter Scoped lifespan, an error is thrown.

To get round this, we need to change the lifespan of the DbContextOptions to also be 'Singleton'. This can be done using by explicitly setting the scope of the DbContextOptions parameter of AddDbContext()

services.AddDbContext<FusionContext>(options =>
    options.UseSqlServer(YourSqlConnection),
    optionsLifetime: ServiceLifetime.Singleton);

There's a really good discussion of this on the EF core GitHub repository starting here. It's also well worth having a look at the source-code for DbContextFactory here.

Alternatively, you can also change the lifetime of the DbContextFactory by setting the ServiceLifetime parameter in the constructor:

services.AddDbContextFactory<FusionContext>(options => 
    options.UseSqlServer(YourSqlConnection), 
    ServiceLifetime.Scoped);

The options should be configured exactly as you would for a normal DbContext as those are the options that will be set on the DbContext the factory creates.

The difference here comes from using token.ThrowIfCancellationRequested(). This method checks for cancellation and if requested throws OperationCanceledException specifically and not TaskCanceledException (understandable as CancellationToken isn't exclusive to the TPL). You can look at the reference source and see that it calls this method:

private void ThrowOperationCanceledException()
{
    throw new OperationCanceledException(Environment.GetResourceString("OperationCanceled"), this);
}

"Regular" cancellation though will indeed generate a TaskCanceledException. You can see that by cancelling the token before the task had a chance to start running:

cancellationTokenSource.Cancel();
var task = Task.Run(() => { }, cancellationTokenSource.Token);
try
{
    await task; 
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
    Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}");
    Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}");
    Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}");
}

Output:

System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Sandbox.Program.<MainAsync>d__1.MoveNext()
Task.IsCanceled: True
Task.IsFaulted: False
Task.Exception: null

Traditional .Net methods usually don't use CancellationToken.ThrowIfCancellationRequested for async API as this is only appropriate when offloading work to another thread. These methods are for inherently asynchronous operations so cancellation is monitored using CancellationToken.Register (or the internal InternalRegisterWithoutEC).