Skip to content

Exceptions and exception handling

Jussi Saarivirta edited this page Jul 25, 2020 · 12 revisions

The Exception model

To help the user to figure out what exactly went wrong and to help in taking corrective actions the Exception model tries to throw meaningful and detailed exceptions. Since there are multiple different batching modes and therefore different behavior and different ways for the operations to fail, there are multiple different Exception types. In addition to those, a few general Exception types exist.

This description tries to explain when to expect each type of exception and how to get information out of them.

General Exception types

System.ArgumentNullException

This exception will be thrown from the constructor if some required parameter is null.

AzureTableDataStoreConfigurationException

This exception is thrown from the constructor if something in the constructor initialization phase fails.

AzureTableDataStoreEntityValidationException<TData>

This exception is thrown when entity validation fails in insert, replace or merge operations, batched and non-batched, when UseClientSideValidation is set to true. The EntityValidationErrors property is a Dictionary<TData, List<string>> that has each entity with validation issues as key and the list of validation issues as value. So from there you can pick the ones that caused the issues.

Example:

var entity = new MyEntity()
{
    PartitionKey = "Secret Agents",
    RowKey = "James Bond\t007", // contains an illegal char, \t
};

store.UseClientSideValidation = true;

try 
{
    await store.InsertAsync(BatchingMode.None, entity);
}
catch(AzureTableDataStoreEntityValidationException<MyEntity> e)
{
    List<string> validationErrors = e.EntityValidationErrors[entity];
    // validationErrors[0] is "Row key contains illegal characters"
    // Do something...
}

AzureTableDataStoreInternalException

A generic exception, when no specific exception type fits the context. These are thrown for example in situations like failing to initialize a table or a blob container, failing to translate a LINQ query expression to a table query, invalid parameters, and generally from all methods before any table or blob operations have occurred.

The InnerException usually contains the actual source of the issue.

AzureTableDataStoreSingleOperationException<TData>

Represents a failure of a single operation. This is thrown from insert, replace, merge and delete operations when there's only one entity to process. The exception contains Entity property of type TData that holds the reference to the failing entity, and a list of blob operation exceptions, BlobOperationExceptions with type List<AzureTableDataStoreBlobOperationException<TData>> that holds any blob operation errors if there were any when processing this insert/merge/replace/delete. If the error wasn't a blob operation error (the table operation itself failed for example), the InnerException usually holds the actual exception that caused the failure.

Example:

// Setting up a failure.
var explodingStream = new Mock<Stream>();
mockStream.Setup(s => s.CanRead).Returns(true);
mockStream.Setup(s => s.Seek(It.IsAny<long>(), It.IsAny<SeekOrigin>()))
    .Throws(new Exception("BOOM!")); // this will get called and throws
mockStream.Setup(s => s.Length).Returns(100);

var entity = new MyEntity()
{
    PartitionKey = "Secret Agents",
    RowKey = "James Bond 007",
    MyBlob = new LargeBlob("image.png", explodingStream.Object, "image/png")
};

try
{
    await store.InsertAsync(BatchingMode.None, entity);
}
catch(AzureTableDataStoreSingleOperationException<MyEntity> e)
{
    var entityWithIssues = e.Entity; // The entity we were inserting
    var innerException = e.InnerException; // The exception that blew things up
    if(e.BlobOperationExceptions.Any()) // this will be true
    {
        LargeBlob blob = e.BlobOperationExceptions[0].SourceBlob; // blob that caused the issue
        MyEntity ent = e.BlobOperationExceptions[0].SourceEntity; // the parent entity
        Exception ex = e.BlobOperationExceptions[0].InnerException; // Exception "BOOM!"
    }
}

AzureTableDataStoreMultiOperationException<TData>

Represents a failure of one or more operations in a set of multiple non-batched operations. This is thrown from insert, replace, merge and delete operations when multiple entities are given to the method to handle with BatchingMode.None.

The exception is effectively a holder for multiple AzureTableDataStoreSingleOperationException<TData> exceptions. The SingleOperationExceptions property holds a list of them.

Example:

MyEntity[] entities = { ... };

try
{
    await store.InsertAsync(BatchingMode.None, entities);
}
catch(AzureTableDataStoreMultiOperationException<MyEntity> e)
{
    foreach(AzureTableDataStoreSingleOperationException<MyEntity> ex in e.SingleOperationExceptions)
    {
        // Handle each exception.
    }
}

AzureTableDataStoreBatchedOperationException<TData>

Represents a failure within a batched operation. Thrown when there's an error stemming from table operations or blob operations or data serialization while processing batch/sub-batches of entities. Thrown when selected batching mode is BatchingMode.Strict, BatchingMode.Strong and BatchingMode.Loose.

The exception is an aggregate of multiple exceptions. The BatchExceptionContexts property contains a list of structures of type BatchExceptionContext<TData> that contain details of each sub-batch failures (for clarification: for example, a batch request of 150 entities gets split to 2 sub-batches because a table API batch call may only contain max 100 entities, so there is one BatchExceptionContext per sub-batch making it a total of 2 in this case).

Each BatchExceptionContext instance holds the list of entities that were contained in that batch in the property BatchEntities, then also the table operation exception if there was one in the TableOperationException property, a list of raised AzureTableDataStoreBlobOperationException<TData> exceptions in the property BlobOperationExceptions and finally in the case of serialization issues, the failing entity in the CurrentEntity property.

Example:

// Let's imagine that we have 150 entities here, of which many
// will have issues and both sub-batches will fail.
MyEntity[] entities = { ... };

try
{
    await store.InsertAsync(BatchingMode.Strong, entities);
}
catch(AzureTableDataStoreBatchedOperationException<MyEntity> e)
{
    // Top level failure information
    _logger.Log(e.Message);
    _logger.Log(e.InnerException);

    // Contexts hold sub-batch level information
    foreach(BatchExceptionContext<MyEntity> batchContext in e.BatchExceptionContexts)
    {
        _logger.Log("This batch of entities had insert issues: " +
            string.Join(",", batchContext.BatchEntities.Select(x => x.Id));

        if(batchContext.TableOperationException != null)
            _logger.Log("The batch insertion table operation failed: " +
                batchContext.TableOperationException.Message;

        if(batchContext.BlobOperationExceptions.Any())
        {
            var errorMessages = batchContext.BlobOperationExceptions.Select(x => 
                $"Entity id {x.SourceEntity.Id} blob operation for {x.SourceBlob.Filename} had an exception: " +
                x.InnerException.Message);
            _logger.Log(string.Join(";", errorMessages));
        }
    }
}

AzureTableDataStoreBlobOperationException<TData>

This exception should never be thrown directly; instead other exceptions may have collections of these inside them. These are thrown within TableDataStore when a blob storage operation fails.

The exception holds a reference to the LargeBlob instance in the SourceBlob property that was the subject of the operation, as well as the entity itself in the SourceEntity property that the LargeBlob instance belongs to.

The InnerException holds the exception that caused the failure.

AzureTableDataStoreQueryException

This exception represents a failure while executing or preparing to execute a query. The InnerException property holds the real reason for the failure. Issues like failing to translate the LINQ query expression to a table filter can cause this, as well as any issues with the table storage itself.