Created
January 21, 2025 05:54
-
-
Save zianwar/0f83d96b29c28552c239c7e3cfe83b8d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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