Skip to content

Instantly share code, notes, and snippets.

@zianwar
Created January 21, 2025 05:54
Show Gist options
  • Save zianwar/0f83d96b29c28552c239c7e3cfe83b8d to your computer and use it in GitHub Desktop.
Save zianwar/0f83d96b29c28552c239c7e3cfe83b8d to your computer and use it in GitHub Desktop.
using Azure;
using Azure.Data.Tables;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Polly;
using System.Runtime.CompilerServices;
public class AzureTableServiceException : Exception
{
public AzureTableServiceException(string message, Exception inner)
: base(message, inner) { }
}
public class EntityNotFoundException : Exception
{
public EntityNotFoundException(string message)
: base(message) { }
}
public class AzureTableSettings
{
public string ConnectionString { get; set; } = string.Empty;
public string TableName { get; set; } = string.Empty;
public int CacheTimeoutMinutes { get; set; } = 5;
public int MaxRetries { get; set; } = 3;
}
public class BaseTableEntity : ITableEntity
{
public string PartitionKey { get; set; } = string.Empty;
public string RowKey { get; set; } = string.Empty;
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}
public interface IAzureTableService<T> where T : class, ITableEntity, new()
{
Task<T?> GetEntityAsync(string partitionKey, string rowKey);
Task<IEnumerable<T>> GetAllAsync(string? partitionKey = null);
Task<Response> InsertOrUpdateAsync(T entity);
Task<Response> DeleteAsync(string partitionKey, string rowKey);
}
public class AzureTableService<T> : IAzureTableService<T> where T : class, ITableEntity, new()
{
private readonly TableClient _tableClient;
private readonly IMemoryCache _cache;
private readonly AzureTableSettings _settings;
private readonly IAsyncPolicy _retryPolicy;
public AzureTableService(
IOptions<AzureTableSettings> settings,
IMemoryCache cache)
{
_settings = settings.Value;
_cache = cache;
// Initialize Table Client
_tableClient = new TableClient(
_settings.ConnectionString,
_settings.TableName);
// Create retry policy
_retryPolicy = Policy
.Handle<RequestFailedException>()
.WaitAndRetryAsync(
_settings.MaxRetries,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
private string GetCacheKey([CallerMemberName] string? method = null, params object[] parameters)
{
return $"{typeof(T).Name}:{method}:{string.Join(":", parameters)}";
}
public async Task<T?> GetEntityAsync(string partitionKey, string rowKey)
{
var cacheKey = GetCacheKey(nameof(GetEntityAsync), partitionKey, rowKey);
if (_cache.TryGetValue(cacheKey, out T? cachedEntity))
{
return cachedEntity;
}
try
{
var response = await _retryPolicy.ExecuteAsync(async () =>
await _tableClient.GetEntityAsync<T>(partitionKey, rowKey));
var entity = response.Value;
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_settings.CacheTimeoutMinutes));
_cache.Set(cacheKey, entity, cacheOptions);
return entity;
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
return null;
}
catch (Exception ex)
{
throw new AzureTableServiceException(
$"Error retrieving entity. PartitionKey: {partitionKey}, RowKey: {rowKey}", ex);
}
}
public async Task<IEnumerable<T>> GetAllAsync(string? partitionKey = null)
{
var cacheKey = GetCacheKey(nameof(GetAllAsync), partitionKey ?? "all");
if (_cache.TryGetValue(cacheKey, out IEnumerable<T>? cachedEntities))
{
return cachedEntities;
}
try
{
var query = _tableClient.QueryAsync<T>();
if (!string.IsNullOrEmpty(partitionKey))
{
query = _tableClient.QueryAsync<T>(x => x.PartitionKey == partitionKey);
}
var entities = await _retryPolicy.ExecuteAsync(async () =>
{
var results = new List<T>();
await foreach (var entity in query)
{
results.Add(entity);
}
return results;
});
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_settings.CacheTimeoutMinutes));
_cache.Set(cacheKey, entities, cacheOptions);
return entities;
}
catch (Exception ex)
{
throw new AzureTableServiceException(
$"Error retrieving entities. PartitionKey filter: {partitionKey}", ex);
}
}
public async Task<Response> InsertOrUpdateAsync(T entity)
{
try
{
var response = await _retryPolicy.ExecuteAsync(async () =>
await _tableClient.UpsertEntityAsync(entity));
// Invalidate cache for the affected partition
var cacheKey = GetCacheKey(nameof(GetAllAsync), entity.PartitionKey);
_cache.Remove(cacheKey);
// Invalidate cache for the specific entity
cacheKey = GetCacheKey(nameof(GetEntityAsync), entity.PartitionKey, entity.RowKey);
_cache.Remove(cacheKey);
return response;
}
catch (Exception ex)
{
throw new AzureTableServiceException(
$"Error upserting entity. PartitionKey: {entity.PartitionKey}, RowKey: {entity.RowKey}", ex);
}
}
public async Task<Response> DeleteAsync(string partitionKey, string rowKey)
{
try
{
var response = await _retryPolicy.ExecuteAsync(async () =>
await _tableClient.DeleteEntityAsync(partitionKey, rowKey));
// Invalidate cache for the affected partition
var cacheKey = GetCacheKey(nameof(GetAllAsync), partitionKey);
_cache.Remove(cacheKey);
// Invalidate cache for the specific entity
cacheKey = GetCacheKey(nameof(GetEntityAsync), partitionKey, rowKey);
_cache.Remove(cacheKey);
return response;
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
throw new EntityNotFoundException(
$"Entity not found. PartitionKey: {partitionKey}, RowKey: {rowKey}");
}
catch (Exception ex)
{
throw new AzureTableServiceException(
$"Error deleting entity. PartitionKey: {partitionKey}, RowKey: {rowKey}", ex);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment