Compare commits

...

10 Commits

8 changed files with 108 additions and 52 deletions

View File

@ -22,7 +22,7 @@ namespace Core.Blueprint.KeyVault
/// <returns>
/// A <see cref="Tuple"/> containing a status message and a boolean indicating whether the secret was successfully deleted.
/// </returns>
ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken);
ValueTask<(string Message, bool Deleted)> DeleteSecretAsync(string secretName, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a secret from Azure Key Vault.
@ -33,7 +33,7 @@ namespace Core.Blueprint.KeyVault
/// A <see cref="Tuple"/> containing the <see cref="KeyVaultResponse"/> with secret details
/// and an optional error message if the secret was not found.
/// </returns>
ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken);
ValueTask<(KeyVaultResponse Secret, string? Message)> GetSecretAsync(string secretName, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned.
@ -43,6 +43,6 @@ namespace Core.Blueprint.KeyVault
/// <returns>
/// A <see cref="Tuple"/> containing the updated <see cref="KeyVaultResponse"/> and an optional error message if the secret was not found.
/// </returns>
ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken);
ValueTask<(KeyVaultResponse Secret, string? Message)> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken);
}
}

View File

@ -1,10 +1,10 @@
using Azure.Security.KeyVault.Secrets;
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
using Core.Blueprint.KeyVault.Configuration;
using Microsoft.Extensions.Configuration;
using System.Net.Http.Json;
using VaultSharp;
using VaultSharp.Core;
using VaultSharp.V1.AuthMethods.Token;
namespace Core.Blueprint.KeyVault;
@ -67,7 +67,7 @@ public sealed class KeyVaultProvider : IKeyVaultProvider
/// <returns>
/// A <see cref="Tuple"/> containing a status message and a boolean indicating whether the secret was successfully deleted.
/// </returns>
public async ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken)
public async ValueTask<(string Message, bool Deleted)> DeleteSecretAsync(string secretName, CancellationToken cancellationToken)
{
if (environment == "Local")
{
@ -88,7 +88,7 @@ public sealed class KeyVaultProvider : IKeyVaultProvider
/// <summary>
/// Retrieves a secret from Azure Key Vault or HashiCorp Vault.
/// </summary>
public async ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken)
public async ValueTask<(KeyVaultResponse Secret, string? Message)> GetSecretAsync(string secretName, CancellationToken cancellationToken)
{
if (environment == "Local")
{
@ -108,7 +108,7 @@ public sealed class KeyVaultProvider : IKeyVaultProvider
}
catch (VaultSharp.Core.VaultApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
{
return new(new KeyVaultResponse(), "Key Not Found");
return new(new KeyVaultResponse { }, "Key Not Found");
}
}
@ -126,7 +126,7 @@ public sealed class KeyVaultProvider : IKeyVaultProvider
/// <summary>
/// Updates an existing secret in Azure Key Vault or HashiCorp Vault. If the secret does not exist, an error is returned.
/// </summary>
public async ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken)
public async ValueTask<(KeyVaultResponse Secret, string? Message)> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken)
{
var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken);
if (!string.IsNullOrEmpty(existingSecret.Item2))

View File

@ -13,6 +13,8 @@ namespace Core.Blueprint.Logging
/// </summary>
public static class MimeTypes
{
public const string ApplicationVersion = "1.0";
/// <summary>
/// The service application/json mime type.
/// </summary>

View File

@ -104,11 +104,13 @@ namespace Core.Blueprint.Mongo
void ReplaceOne(TDocument document);
/// <summary>
/// Asynchronously replaces an existing document with a new one.
/// Asynchronously replaces an existing document in the collection and returns the updated version.
/// </summary>
/// <param name="document">The document to replace the existing one.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task ReplaceOneAsync(TDocument document);
/// <param name="document">The document with the updated data. Its _Id is used to locate the existing document.</param>
/// <returns>
/// The updated document if the replacement was successful; otherwise, <c>null</c> if no matching document was found.
/// </returns>
Task<TDocument?> ReplaceOneAsync(TDocument document);
/// <summary>
/// Deletes a single document by the provided filter expression.

View File

@ -175,16 +175,27 @@ namespace Core.Blueprint.Mongo
}
/// <summary>
/// Asynchronously replaces an existing document in the collection.
/// Asynchronously replaces an existing document in the collection and returns the updated version.
/// </summary>
/// <param name="document">The document with the updated data.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public virtual async Task ReplaceOneAsync(TDocument document)
/// <param name="document">The document with the updated data. Its _Id is used to locate the existing document.</param>
/// <returns>
/// The updated document if the replacement was successful; otherwise, <c>null</c> if no matching document was found.
/// </returns>
public virtual async Task<TDocument?> ReplaceOneAsync(TDocument document)
{
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, document._Id);
await _collection.FindOneAndReplaceAsync(filter, document);
var options = new FindOneAndReplaceOptions<TDocument>
{
ReturnDocument = ReturnDocument.After // return the updated document
};
var result = await _collection.FindOneAndReplaceAsync(filter, document, options);
return result;
}
/// <summary>
/// Deletes a single document from the collection based on the provided filter expression.
/// </summary>

View File

@ -27,7 +27,7 @@ namespace Core.Blueprint.Redis.Configuration
// Register RedisCacheProvider
services.AddSingleton<IRedisCacheProvider>(provider =>
new RedisCacheProvider(redisConnectionString, provider.GetRequiredService<ILogger<RedisCacheProvider>>()));
new RedisCacheProvider(redisConnectionString, provider.GetRequiredService<ILogger<RedisCacheProvider>>(), configuration));
// Get CacheSettings and register with the ICacheSettings interface
var cacheSettings = configuration.GetSection("CacheSettings").Get<CacheSettings>();

View File

@ -1,4 +1,5 @@
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using System.Text.Json;
@ -12,6 +13,7 @@ namespace Core.Blueprint.Redis
{
private IDatabase _cacheDatabase = null!;
private readonly ILogger<RedisCacheProvider> _logger;
private readonly bool _useRedis;
/// <summary>
/// Initializes a new instance of the <see cref="RedisCacheProvider"/> class.
@ -19,35 +21,53 @@ namespace Core.Blueprint.Redis
/// <param name="connectionString">The Redis connection string.</param>
/// <param name="logger">The logger instance for logging operations.</param>
/// <exception cref="ArgumentNullException">Thrown when connection string is null or empty.</exception>
public RedisCacheProvider(string connectionString, ILogger<RedisCacheProvider> logger)
public RedisCacheProvider(string connectionString, ILogger<RedisCacheProvider> logger, IConfiguration configuration)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentNullException(nameof(connectionString), "Redis connection string cannot be null or empty.");
_logger = logger;
_useRedis = configuration.GetValue<bool>("UseRedisCache", false);
_cacheDatabase = InitializeRedisAsync(connectionString).GetAwaiter().GetResult();
}
/// <summary>
/// Initializes and establishes a connection to Redis using the provided connection string.
/// Initializes and establishes a connection to Redis based on the environment.
/// Uses a local connection in development, and Azure with token credentials in other environments.
/// </summary>
/// <param name="connectionString">The Redis connection string.</param>
/// <returns>An <see cref="IDatabase"/> instance representing the Redis cache database.</returns>
/// <exception cref="Exception">Thrown when the connection to Redis fails.</exce
async Task<IDatabase> InitializeRedisAsync(string connectionString)
/// <exception cref="Exception">Thrown when the connection to Redis fails.</exception>
async Task<IDatabase?> InitializeRedisAsync(string connectionString)
{
try
{
var configurationOptions = await ConfigurationOptions.Parse($"{connectionString}")
if (_useRedis)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty;
ConnectionMultiplexer connectionMultiplexer;
if (environment.Equals("Local", StringComparison.OrdinalIgnoreCase))
{
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(connectionString);
}
else
{
var configurationOptions = await ConfigurationOptions.Parse(connectionString)
.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential());
configurationOptions.AbortOnConnectFail = false;
var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions);
}
_logger.LogInformation("Successfully connected to Redis.");
return connectionMultiplexer.GetDatabase();
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error establishing Redis connection.");
@ -64,8 +84,11 @@ namespace Core.Blueprint.Redis
public async ValueTask<TEntity> GetAsync<TEntity>(string key)
{
try
{
if (_useRedis is not false)
{
var value = await _cacheDatabase.StringGetAsync(key);
if (value.IsNullOrEmpty)
{
_logger.LogInformation($"Cache miss for key: {key}");
@ -75,6 +98,9 @@ namespace Core.Blueprint.Redis
_logger.LogInformation($"Cache hit for key: {key}");
return JsonSerializer.Deserialize<TEntity>(value);
}
return default;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting cache item with key {key}");
@ -91,11 +117,14 @@ namespace Core.Blueprint.Redis
public async ValueTask SetAsync<TEntity>(string key, TEntity value, TimeSpan? expiry = null)
{
try
{
if (_useRedis is not false)
{
var json = JsonSerializer.Serialize(value);
await _cacheDatabase.StringSetAsync(key, json, expiry);
_logger.LogInformation($"Cache item set with key: {key}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error setting cache item with key {key}");
@ -110,10 +139,13 @@ namespace Core.Blueprint.Redis
public async ValueTask RemoveAsync(string key)
{
try
{
if (_useRedis is not false)
{
await _cacheDatabase.KeyDeleteAsync(key);
_logger.LogInformation($"Cache item removed with key: {key}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error removing cache item with key {key}");
@ -129,10 +161,14 @@ namespace Core.Blueprint.Redis
public async ValueTask<bool> ExistsAsync(string key)
{
try
{
if (_useRedis is not false)
{
var exists = await _cacheDatabase.KeyExistsAsync(key);
_logger.LogInformation($"Cache item exists check for key: {key} - {exists}");
return exists;
}
return false;
}
catch (Exception ex)
{
@ -149,6 +185,8 @@ namespace Core.Blueprint.Redis
public async ValueTask RefreshAsync(string key, TimeSpan? expiry = null)
{
try
{
if (_useRedis is not false)
{
var value = await _cacheDatabase.StringGetAsync(key);
if (!value.IsNullOrEmpty)
@ -161,6 +199,7 @@ namespace Core.Blueprint.Redis
_logger.LogWarning($"Cache item with key: {key} does not exist, cannot refresh");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error refreshing cache item with key {key}");

View File

@ -18,6 +18,8 @@ namespace Core.Blueprint.SQLServer.Configuration
/// <returns>An updated <see cref="IServiceCollection"/> with SQL Server services registered.</returns>
public static IServiceCollection AddSQLServer(this IServiceCollection services, IConfiguration configuration)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty;
var chainedCredentials = new ChainedTokenCredential(
new ManagedIdentityCredential(),
new SharedTokenCacheCredential(),