Migration from the legacy Azure Cosmos Tables library to the modern one.

Serhii Korol - Aug 13 '23 - - Dev Community

I want to tell you about Cosmos Db and how to improve your code. Probably you have already faced the case when you saw that Microsoft.Azure.Cosmos.Table package is deprecated. It means you need to migrate to the modern Azure.Data.Tables package. I'll tell you as you can to do it.
To clarify this, I created a solution and two simple console projects. Both projects have, by class where implemented the CRUD functionality of the Cosmos Tables Database. At each step, I'll explain what was changed or added. Let's go.

Initial

For convenience, I added creating a client to the constructor and we do not need to pass a connection string when we'll want something to do. However, there exist some differences. As you are able to see, the modern implementations are more shortest. We no longer need to parse the connection string. We can at once initial a client.

private readonly CloudTableClient _client;
public LegacyCosmosTable(string connectionString)
{
    var storageAccount = CloudStorageAccount.Parse(connectionString);
_client = storageAccount.CreateCloudTableClient();
}


private readonly TableServiceClient _client;
public ModernCosmosTable(string connectionString)
{
    _client = new TableServiceClient(connectionString);
}

Enter fullscreen mode Exit fullscreen mode

Before we start implement a first CRUD function, let's create repeatable call for each function.

//legacy
private CloudTable GetCloudTable(string tableName)
{
    var table = _client.GetTableReference(tableName);
    return table;
}

//modern
private TableClient GetTableClient(string tableName)
{
    var tableClient = _client.GetTableClient(tableName);
    return tableClient;
}
Enter fullscreen mode Exit fullscreen mode

Create

The Cosmos Db is able to check if the table already exists then returns the table or creates a new one.

//legacy
public async Task<CloudTable> CreateTableIfToExistsAsync(string tableName)
{
    var table = _client.GetTableReference(tableName);
    await     table.CreateIfNotExistsAsync().ConfigureAwait(false);
    return table;
}

//modern
public async Task<TableClient> CreateTableIfToExistsAsync(string tableName)
{
    var table = _client.GetTableClient(tableName);
    await table.CreateIfNotExistsAsync().ConfigureAwait(false);
    return table;
}
Enter fullscreen mode Exit fullscreen mode

Read

Let's implement getting all entities. Compare how much shortest and easiest modern implementation. We also used ITableEntity instead TableEntity.

//legacy
public async Task<IEnumerable<TEntity>> GetAllAsync<TEntity>(string tableName) where TEntity : TableEntity, new()
{
    var table = GetCloudTable(tableName);
    var tableQuery = new TableQuery<TEntity>();
    var results = new List<TEntity>();
    TableContinuationToken? token = null;
    do
    {
        var segment = await table.ExecuteQuerySegmentedAsync(tableQuery, token).ConfigureAwait(false);
        token = segment.ContinuationToken;
        results.AddRange(segment);
    } while (token != null);
    return results;
}

//modern
public async Task<IEnumerable<TEntity>> GetAllAsync<TEntity>(string tableName)
        where TEntity : class, ITableEntity, new()
{
    TableClient tableClient = GetTableClient(tableName);
    var results = new List<TEntity>();
    await foreach (var entity in tableClient.QueryAsync<TEntity>())
    {
        results.Add(entity);
    }
    return result;
}
Enter fullscreen mode Exit fullscreen mode

It's everything good, but if I want to filter data? Let's create the next function. How you can see, the both methods similar and have two filters, by partition key and columns which we want to select.

//legacy
public async Task<IEnumerable<TEntity>> GetByPartitionKeyAsync<TEntity>(string tableName, string partitionKey,
            string[]? columns = null) where TEntity : TableEntity, new()
{
    var table = GetCloudTable(tableName);
    TableQuery<TEntity> tableQuery;
    if (columns != null)
    {
        tableQuery = new TableQuery<TEntity>()
                 .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey))
                    .Select(columns);
    }
    else
    {
        tableQuery = new TableQuery<TEntity>()
                    .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
    }
    var results = new List<TEntity>();
    TableContinuationToken token = null;
    do
    {
        var segment = await table.ExecuteQuerySegmentedAsync(tableQuery, token).ConfigureAwait(false);
        token = segment.ContinuationToken;
        results.AddRange(segment);
    } while (token != null);
    return results;
}

//modern
public async Task<IEnumerable<TEntity>> GetByPartitionKeyAsync<TEntity>(string tableName, string partitionKey,
        string[]? columns = null) where TEntity : class, ITableEntity, new()
{
    TableClient tableClient = GetTableClient(tableName);
    var results = new List<TEntity>();
    if (columns != null)
    {
        await foreach (var entity in tableClient.QueryAsync<TEntity>(filter => filter.PartitionKey == partitionKey,
                               select: columns))
        {
            results.Add(entity);
        }
    }
    else
    {
        await foreach (var entity in tableClient.QueryAsync<TEntity>(filter => filter.PartitionKey == partitionKey))
        {
            results.Add(entity);
        }
    }
    return results;
}
Enter fullscreen mode Exit fullscreen mode

If you want to get only one entity, we need to pass a key row, then see here:

//legacy
public async Task<TEntity> GetAsync<TEntity>(string tableName, string partitionKey, string rowKey)
            where TEntity : TableEntity
{
    var table = GetCloudTable(tableName);
    var get = TableOperation.Retrieve<TEntity>(partitionKey, rowKey);
    var result = await table.ExecuteAsync(get).ConfigureAwait(false);

    return (TEntity)result.Result;
}

//modern
public async Task<TEntity> GetAsync<TEntity>(string tableName, string partitionKey, string rowKey)
        where TEntity : class, ITableEntity
{
    TableClient tableClient = GetTableClient(tableName);
    var result = await tableClient.GetEntityAsync<TEntity>(partitionKey, rowKey).ConfigureAwait(false);
    return result;
}
Enter fullscreen mode Exit fullscreen mode

Update

And now, let's consider updating the tables. There exist some differences between databases, unlike relational databases. Here we can replace or merge data. Let's implement inserting with merging. It is worth noticing, the modern approach merges data by default and this method shortest and easiest.

//legacy
public async Task InsertOrMergeAsync(string tableName, TableEntity entity)
{
    var table = GetCloudTable(tableName);
    var insert = TableOperation.InsertOrMerge(entity);
    await table.ExecuteAsync(insert).ConfigureAwait(false);
}

//modern
public async Task InsertOrMergeAsync(string tableName, ITableEntity entity)
{
    TableClient tableClient = GetTableClient(tableName);
    await tableClient.UpsertEntityAsync(entity);
}
Enter fullscreen mode Exit fullscreen mode

If you need only insert data, you can use the code below. However in the modern approach you should the pass ETag property. You can read about it by the link.

//legacy
public async Task InsertAsync(string tableName, TableEntity entity)
{
    var table = _client.GetTableReference(tableName);
    var insert = TableOperation.Insert(entity);
    await table.ExecuteAsync(insert).ConfigureAwait(false);
}

//modern
public async Task InsertAsync(string tableName, ITableEntity entity)
{
    TableClient tableClient = GetTableClient(tableName);
    await tableClient.UpdateEntityAsync(entity, entity.ETag);
}
Enter fullscreen mode Exit fullscreen mode

What if you want to insert data and this data exists then merge? It's similar to previous methods. In the modern approach, you need only add the flag of replacing.

//legacy
public async Task InsertOrReplaceAsync(string tableName, TableEntity entity)
{
    var table = GetCloudTable(tableName);
    var insert = TableOperation.InsertOrReplace(entity);
    await table.ExecuteAsync(insert).ConfigureAwait(false);
}

//modern
public async Task InsertOrReplaceAsync(string tableName, ITableEntity entity)
{
    TableClient tableClient = GetTableClient(tableName);
    await tableClient.UpsertEntityAsync(entity, TableUpdateMode.Replace);
}
Enter fullscreen mode Exit fullscreen mode

Delete

And now we came up to delete. Let's consider batch deleting.

//legacy
public async Task<IList<TableResult>> DeleteByPartitionKeyBatchAsync<TEntity>(string tableName,
            string partitionKey, string[]? columns = null) where TEntity : TableEntity, new()
{
    IList<TableResult> results = new List<TableResult>();
    IEnumerable<TEntity> entities =
                await GetByPartitionKeyAsync<TEntity>(tableName, partitionKey, columns)
                    .ConfigureAwait(false);
    if (entities.Any())
    {
        results = await DeleteBatchAsync(tableName, entities).ConfigureAwait(false);
    }
    return results;
}

//modern
public async Task<Response<IReadOnlyList<Response>>?> DeleteByPartitionKeyBatchAsync<TEntity>(string tableName,
        string partitionKey, string[]? columns = null) where TEntity : class, ITableEntity, new()
{
    Response<IReadOnlyList<Response>>? results;
    IEnumerable<TEntity> entities = await GetByPartitionKeyAsync<TEntity>(tableName, partitionKey, columns)
            .ConfigureAwait(false);
    if (!entities.Any()) return null;
    results = await DeleteBatchAsync(tableName, entities).ConfigureAwait(false);
    return results;
}
Enter fullscreen mode Exit fullscreen mode

However it not all. We need to implement deleting. For this, you need the method below. As you can see, you need to pass ETag that can be NULL.

//legacy
private async Task<IList<TableResult>> DeleteBatchAsync<TEntity>(string tableName, IEnumerable<TEntity> entities)
            where TEntity : TableEntity, new()
{
    var table = GetCloudTable(tableName);
    var batchOperation = new TableBatchOperation();
    foreach (var e in entities)
    {
        e.ETag = e.ETag ?? "*";
        batchOperation.Delete(e);
    }

    return await      table.ExecuteBatchAsync(batchOperation).ConfigureAwait(false);
}

//modern
private async Task<Response<IReadOnlyList<Response>>?> DeleteBatchAsync<TEntity>(string tableName,
        IEnumerable<TEntity> entities) where TEntity : ITableEntity, new()
{
    TableClient tableClient = GetTableClient(tableName);
    var batch = new List<TableTransactionAction>();

    foreach (var entity in entities)
    {
        batch.Add(new TableTransactionAction(TableTransactionActionType.Delete, entity));
    }

    return await tableClient.SubmitTransactionAsync(batch).ConfigureAwait(false);
}
Enter fullscreen mode Exit fullscreen mode

If you need only one entity then it's more easy.

//legacy
public async Task DeleteEntityAsync(string tableName, TableEntity entity)
{
    var table = GetCloudTable(tableName);
    entity.ETag ??= "*";
    var delete = TableOperation.Delete(entity);
    await table.ExecuteAsync(delete).ConfigureAwait(false);
}

//modern
public async Task DeleteEntityAsync(string tableName, ITableEntity entity)
{
    TableClient tableClient = GetTableClient(tableName);
    await tableClient.DeleteEntityAsync(entity.PartitionKey, entity.RowKey, entity.ETag).ConfigureAwait(false);
}
Enter fullscreen mode Exit fullscreen mode

And now let's clear the table. The Cosmos Db doesn't have a special method for clearing data in the table like TRUNCATE in SQL. However, we have queries that can do it. Both approaches are equal.

//legacy and modern
public async Task ClearTableAsync(string tableName)
{
    var entities = await GetAllAsync<TableEntity>(tableName).ConfigureAwait(false);
    if (entities.Any())
    {
        await DeleteBatchAsync(tableName, entities).ConfigureAwait(false);
    }
}
Enter fullscreen mode Exit fullscreen mode

And sure you might be wanted to delete the table:

//legacy
public async Task DeleteTableAsync(string tableName)
{
    var table = GetCloudTable(tableName);
    await table.DeleteAsync().ConfigureAwait(false);
}

//modern
public async Task DeleteTableAsync(string tableName)
{
    var table = GetTableClient(tableName);
    await table.DeleteAsync().ConfigureAwait(false);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The new API becomes more readable and smallest by implementation. As far as the legacy library is deprecated, I strongly recommend migrating to the latest library taking advantage of this article.
Thank you for reading, put like on, make a subscription, and see you in the next article. Happy coding!

The source code you are able to by link.

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .