Compare commits
	
		
			9 Commits
		
	
	
		
			feature/ad
			...
			feature/im
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 626105cf0c | |||
| eda79010ce | |||
| a56818bcf8 | |||
|   | 5410a9f9a0 | ||
| 140eab163a | |||
|   | d2a8ced972 | ||
|   | f8c6db55e9 | ||
| 398ca3d7b6 | |||
| ffed92e85c | 
| @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Blueprint.KeyVault", " | |||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Mongo", "Core.Blueprint.Mongo\Core.Blueprint.Mongo.csproj", "{27A8E3E1-D613-4D5B-8105-485699409F1E}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Mongo", "Core.Blueprint.Mongo\Core.Blueprint.Mongo.csproj", "{27A8E3E1-D613-4D5B-8105-485699409F1E}" | ||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Redis", "Core.Blueprint.Redis\Core.Blueprint.Redis.csproj", "{11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Caching", "Core.Blueprint.Redis\Core.Blueprint.Caching.csproj", "{11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}" | ||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Storage", "Core.Blueprint.Storage\Core.Blueprint.Storage.csproj", "{636E4520-79F9-46C8-990D-08F2D24A151C}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Storage", "Core.Blueprint.Storage\Core.Blueprint.Storage.csproj", "{636E4520-79F9-46C8-990D-08F2D24A151C}" | ||||||
| EndProject | EndProject | ||||||
|   | |||||||
| @@ -15,6 +15,23 @@ namespace Core.Blueprint.KeyVault.Configuration | |||||||
|     public static class RegisterBlueprint |     public static class RegisterBlueprint | ||||||
|     { |     { | ||||||
|         public static IServiceCollection AddKeyVault(this IServiceCollection services, IConfiguration configuration) |         public static IServiceCollection AddKeyVault(this IServiceCollection services, IConfiguration configuration) | ||||||
|  |         { | ||||||
|  |  | ||||||
|  |             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||||
|  |  | ||||||
|  |             if(environment ==  "Local") | ||||||
|  |             { | ||||||
|  |                 var vaultSettings = configuration.GetSection("Vault").Get<VaultOptions>(); | ||||||
|  |  | ||||||
|  |                 if (string.IsNullOrEmpty(vaultSettings?.Address) || string.IsNullOrEmpty(vaultSettings.Token) | ||||||
|  |                     || string.IsNullOrEmpty(vaultSettings.SecretMount)) | ||||||
|  |                 { | ||||||
|  |                     throw new ArgumentNullException("Vault options are not configured correctly."); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 services.AddSingleton(vaultSettings); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|             { |             { | ||||||
|                 var keyVaultUriString = configuration["ConnectionStrings:KeyVaultDAL"]; |                 var keyVaultUriString = configuration["ConnectionStrings:KeyVaultDAL"]; | ||||||
|  |  | ||||||
| @@ -25,8 +42,8 @@ namespace Core.Blueprint.KeyVault.Configuration | |||||||
|  |  | ||||||
|                 var keyVaultUri = new Uri(keyVaultUriString); |                 var keyVaultUri = new Uri(keyVaultUriString); | ||||||
|  |  | ||||||
|             // Register SecretClient as a singleton |  | ||||||
|                 services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); |                 services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             services.AddSingleton<IKeyVaultProvider, KeyVaultProvider>(); |             services.AddSingleton<IKeyVaultProvider, KeyVaultProvider>(); | ||||||
|             return services; |             return services; | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								Core.Blueprint.KeyVault/Configuration/VaultOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Core.Blueprint.KeyVault/Configuration/VaultOptions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace Core.Blueprint.KeyVault.Configuration | ||||||
|  | { | ||||||
|  |     public class VaultOptions | ||||||
|  |     { | ||||||
|  |         public string Address { get; set; } = string.Empty; | ||||||
|  |         public string Token { get; set; } = string.Empty; | ||||||
|  |         public string SecretMount { get; set; } = string.Empty; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,7 +10,9 @@ | |||||||
|     <PackageReference Include="Azure.Identity" Version="1.13.1" /> |     <PackageReference Include="Azure.Identity" Version="1.13.1" /> | ||||||
|     <PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" /> |     <PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> |     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> | ||||||
|  |     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> |     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> | ||||||
|  |     <PackageReference Include="VaultSharp" Version="1.17.5.1" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
| @@ -1,32 +1,66 @@ | |||||||
| using Azure; | using Azure.Security.KeyVault.Secrets; | ||||||
| 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.Core; | ||||||
|  |  | ||||||
|  | namespace Core.Blueprint.KeyVault; | ||||||
|  |  | ||||||
| namespace Core.Blueprint.KeyVault |  | ||||||
| { |  | ||||||
| /// <summary> | /// <summary> | ||||||
|     /// Provides operations for managing secrets in Azure Key Vault. | /// Provides operations for managing secrets in Azure Key Vault or HashiCorp Vault transparently based on the environment. | ||||||
| /// </summary> | /// </summary> | ||||||
|     public sealed class KeyVaultProvider(SecretClient keyVaultProvider): IKeyVaultProvider | public sealed class KeyVaultProvider : IKeyVaultProvider | ||||||
| { | { | ||||||
|         /// <summary> |     private readonly string environment; | ||||||
|         /// Creates a new secret in Azure Key Vault. |     private readonly SecretClient? azureClient; | ||||||
|         /// </summary> |     private readonly IVaultClient? hashiClient; | ||||||
|         /// <param name="keyVaultRequest">The request containing the name and value of the secret.</param> |     private readonly VaultOptions? hashiOptions; | ||||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> |  | ||||||
|         /// <returns>A <see cref="KeyVaultResponse"/> containing the details of the created secret.</returns> |     public KeyVaultProvider(IConfiguration configuration) | ||||||
|         public async ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken) |  | ||||||
|     { |     { | ||||||
|             KeyVaultResponse _response = new(); |         environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; | ||||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(keyVaultRequest.Name, keyVaultRequest.Value), cancellationToken); |  | ||||||
|  |  | ||||||
|             _response.Value = azureResponse.Value; |         if (environment == "Local") | ||||||
|             _response.Name = azureResponse.Name; |         { | ||||||
|  |             hashiOptions = configuration.GetSection("Vault").Get<VaultOptions>(); | ||||||
|             return _response; |             hashiClient = new VaultClient(new VaultClientSettings( | ||||||
|  |                 hashiOptions?.Address, | ||||||
|  |                 new TokenAuthMethodInfo(hashiOptions?.Token) | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             var keyVaultUri = new Uri(configuration["ConnectionStrings:KeyVaultDAL"]!); | ||||||
|  |             azureClient = new SecretClient(keyVaultUri, new Azure.Identity.DefaultAzureCredential()); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|         /// Deletes a secret from Azure Key Vault if it exists. |     /// Creates a new secret in Azure Key Vault or HashiCorp Vault. | ||||||
|  |     /// </summary> | ||||||
|  |     public async ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken) | ||||||
|  |     { | ||||||
|  |         if (environment == "Local") | ||||||
|  |         { | ||||||
|  |             await hashiClient!.V1.Secrets.KeyValue.V2.WriteSecretAsync( | ||||||
|  |                 path: keyVaultRequest.Name, | ||||||
|  |                 data: new Dictionary<string, object> { { "value", keyVaultRequest.Value } }, | ||||||
|  |                 mountPoint: hashiOptions!.SecretMount | ||||||
|  |             ); | ||||||
|  |             return new KeyVaultResponse { Name = keyVaultRequest.Name, Value = keyVaultRequest.Value }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         KeyVaultSecret azureResponse = await azureClient!.SetSecretAsync( | ||||||
|  |             new KeyVaultSecret(keyVaultRequest.Name, keyVaultRequest.Value), cancellationToken | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return new KeyVaultResponse { Name = azureResponse.Name, Value = azureResponse.Value }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Permanently deletes a secret from Azure Key Vault or HashiCorp Vault (hard delete for Vault). | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     /// <param name="secretName">The name of the secret to delete.</param> |     /// <param name="secretName">The name of the secret to delete.</param> | ||||||
|     /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> |     /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||||||
| @@ -35,59 +69,120 @@ namespace Core.Blueprint.KeyVault | |||||||
|     /// </returns> |     /// </returns> | ||||||
|     public async ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken) |     public async ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken) | ||||||
|     { |     { | ||||||
|             var existingSecret = await this.GetSecretAsync(secretName, cancellationToken); |         if (environment == "Local") | ||||||
|             if (existingSecret != null) |  | ||||||
|         { |         { | ||||||
|                 await keyVaultProvider.StartDeleteSecretAsync(secretName, cancellationToken); |             await DestroyAllSecretVersionsAsync(secretName, cancellationToken); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var existingSecret = await this.GetSecretAsync(secretName, cancellationToken); | ||||||
|  |         if (existingSecret.Item2 == string.Empty) | ||||||
|  |         { | ||||||
|  |             await azureClient!.StartDeleteSecretAsync(secretName, cancellationToken); | ||||||
|             return new("Key Deleted", true); |             return new("Key Deleted", true); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return new("Key Not Found", false); |         return new("Key Not Found", false); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|         /// Retrieves a secret from Azure Key Vault. |     /// Retrieves a secret from Azure Key Vault or HashiCorp Vault. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|         /// <param name="secretName">The name of the secret to retrieve.</param> |  | ||||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> |  | ||||||
|         /// <returns> |  | ||||||
|         /// A <see cref="Tuple"/> containing the <see cref="KeyVaultResponse"/> with secret details  |  | ||||||
|         /// and an optional error message if the secret was not found. |  | ||||||
|         /// </returns> |  | ||||||
|     public async ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken) |     public async ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken) | ||||||
|     { |     { | ||||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.GetSecretAsync(secretName, cancellationToken: cancellationToken); |         if (environment == "Local") | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var secret = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretAsync( | ||||||
|  |                     path: secretName, | ||||||
|  |                     mountPoint: hashiOptions!.SecretMount | ||||||
|  |                 ); | ||||||
|  |  | ||||||
|             if (azureResponse == null) |                 if (secret.Data.Data.TryGetValue("value", out var value)) | ||||||
|  |                 { | ||||||
|  |                     return new(new KeyVaultResponse { Name = secretName, Value = value?.ToString() ?? "" }, string.Empty); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return new(new KeyVaultResponse(), "Key Not Found"); | ||||||
|  |             } | ||||||
|  |             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"); | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             KeyVaultSecret azureResponse = await azureClient!.GetSecretAsync(secretName, cancellationToken: cancellationToken); | ||||||
|             return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty); |             return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty); | ||||||
|         } |         } | ||||||
|  |         catch (Azure.RequestFailedException ex) when (ex.Status == 404) | ||||||
|  |         { | ||||||
|  |             return new(new KeyVaultResponse(), "Key Not Found"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|         /// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned. |     /// Updates an existing secret in Azure Key Vault or HashiCorp Vault. If the secret does not exist, an error is returned. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|         /// <param name="newSecret">The updated secret information.</param> |  | ||||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> |  | ||||||
|         /// <returns> |  | ||||||
|         /// A <see cref="Tuple"/> containing the updated <see cref="KeyVaultResponse"/> and an optional error message if the secret was not found. |  | ||||||
|         /// </returns> |  | ||||||
|     public async ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken) |     public async ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken) | ||||||
|     { |     { | ||||||
|             KeyVaultResponse _response = new(); |  | ||||||
|         var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken); |         var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken); | ||||||
|             if (existingSecret == null) |         if (!string.IsNullOrEmpty(existingSecret.Item2)) | ||||||
|         { |         { | ||||||
|             return new(new KeyVaultResponse(), "Key Not Found"); |             return new(new KeyVaultResponse(), "Key Not Found"); | ||||||
|         } |         } | ||||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(newSecret.Name, newSecret.Value), cancellationToken); |  | ||||||
|  |  | ||||||
|             _response.Value = azureResponse.Value; |         var updated = await CreateSecretAsync(newSecret, cancellationToken); | ||||||
|             _response.Name = azureResponse.Name; |         return new(updated, string.Empty); | ||||||
|  |     } | ||||||
|  |  | ||||||
|             return new(new KeyVaultResponse { Name = newSecret.Name, Value = azureResponse.Value }, string.Empty); |     /// <summary> | ||||||
|         } |     /// Permanently deletes all versions of a given secret in HashiCorp Vault. | ||||||
|  |     /// Returns a tuple indicating the result status and a message. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="secretName">The secret name/path.</param> | ||||||
|  |     /// <param name="cancellationToken">A cancellation token.</param> | ||||||
|  |     /// <returns> | ||||||
|  |     /// A tuple: | ||||||
|  |     /// - <c>bool?</c>: <c>true</c> if deleted, <c>false</c> if no versions, <c>null</c> if not found. | ||||||
|  |     /// - <c>string</c>: message explaining the result. | ||||||
|  |     /// </returns> | ||||||
|  |     private async Task<(bool? WasDeleted, string Message)> DestroyAllSecretVersionsAsync(string secretName, CancellationToken cancellationToken) | ||||||
|  |     { | ||||||
|  |         Dictionary<string, object> versions; | ||||||
|  |  | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             var metadata = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretMetadataAsync( | ||||||
|  |                 path: secretName, | ||||||
|  |                 mountPoint: hashiOptions!.SecretMount | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             versions = metadata.Data.Versions.Keys.ToDictionary(k => k, _ => (object)0); | ||||||
|  |             if (versions.Count == 0) | ||||||
|  |                 return (false, "Key exists but contains no versions."); | ||||||
|  |         } | ||||||
|  |         catch (VaultApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) | ||||||
|  |         { | ||||||
|  |             return (null, "Key Not Found."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         using var httpClient = new HttpClient { BaseAddress = new Uri(hashiOptions.Address) }; | ||||||
|  |         var request = new HttpRequestMessage(HttpMethod.Post, $"/v1/{hashiOptions.SecretMount}/destroy/{secretName}") | ||||||
|  |         { | ||||||
|  |             Content = JsonContent.Create(new { versions = versions.Keys.ToArray() }) | ||||||
|  |         }; | ||||||
|  |         request.Headers.Add("X-Vault-Token", hashiOptions.Token); | ||||||
|  |         var response = await httpClient.SendAsync(request, cancellationToken); | ||||||
|  |         response.EnsureSuccessStatusCode(); | ||||||
|  |  | ||||||
|  |         await hashiClient.V1.Secrets.KeyValue.V2.DeleteMetadataAsync( | ||||||
|  |             path: secretName, | ||||||
|  |             mountPoint: hashiOptions.SecretMount | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return (true, "Key Permanently Deleted."); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,4 @@ | |||||||
| using System; | namespace Core.Blueprint.Caching.Adapters | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Linq; |  | ||||||
| using System.Text; |  | ||||||
| using System.Threading.Tasks; |  | ||||||
|  |  | ||||||
| namespace Core.Blueprint.Redis |  | ||||||
| { | { | ||||||
|     public interface ICacheSettings |     public interface ICacheSettings | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| using Microsoft.Extensions.Configuration; | using Core.Blueprint.Caching.Adapters; | ||||||
|  | using Core.Blueprint.Caching.Contracts; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  |  | ||||||
| namespace Core.Blueprint.Redis.Configuration | namespace Core.Blueprint.Caching.Configuration | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Provides extension methods for registering Redis-related services in the DI container. |     /// Provides extension methods for registering Redis-related services in the DI container. | ||||||
| @@ -17,23 +19,32 @@ namespace Core.Blueprint.Redis.Configuration | |||||||
|         /// <returns>The updated service collection.</returns> |         /// <returns>The updated service collection.</returns> | ||||||
|         public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) |         public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) | ||||||
|         { |         { | ||||||
|             // Retrieve the Redis connection string from the configuration. |             // TODO for the following variable we'll need to add in the appsettings.json the following config: "UseRedisCache": true, | ||||||
|             // Get Redis configuration section |             bool useRedis = configuration.GetValue<bool>("UseRedisCache"); | ||||||
|  |             //TODO decide wheter to use appsettings or the following ENV variable | ||||||
|  |             useRedis = Environment.GetEnvironmentVariable("CORE_BLUEPRINT_PACKAGES_USE_REDIS")?.ToLower() == "true"; | ||||||
|  |  | ||||||
|  |             if (useRedis) | ||||||
|  |             { | ||||||
|                 var redisConnectionString = configuration.GetSection("ConnectionStrings:Redis").Value; |                 var redisConnectionString = configuration.GetSection("ConnectionStrings:Redis").Value; | ||||||
|                 if (string.IsNullOrEmpty(redisConnectionString)) |                 if (string.IsNullOrEmpty(redisConnectionString)) | ||||||
|                 { |                 { | ||||||
|                     throw new InvalidOperationException("Redis connection is not configured."); |                     throw new InvalidOperationException("Redis connection is not configured."); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|             // Register RedisCacheProvider |                 services.AddSingleton<ICacheProvider>(provider => | ||||||
|             services.AddSingleton<IRedisCacheProvider>(provider => |  | ||||||
|                     new RedisCacheProvider(redisConnectionString, provider.GetRequiredService<ILogger<RedisCacheProvider>>())); |                     new RedisCacheProvider(redisConnectionString, provider.GetRequiredService<ILogger<RedisCacheProvider>>())); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 services.AddMemoryCache(); | ||||||
|  |                 services.AddSingleton<ICacheProvider, MemoryCacheProvider>(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             // Get CacheSettings and register with the ICacheSettings interface |  | ||||||
|             var cacheSettings = configuration.GetSection("CacheSettings").Get<CacheSettings>(); |             var cacheSettings = configuration.GetSection("CacheSettings").Get<CacheSettings>(); | ||||||
|             if (cacheSettings == null) |             if (cacheSettings == null) | ||||||
|             { |             { | ||||||
|                 throw new InvalidOperationException("Redis CacheSettings section is not configured."); |                 throw new InvalidOperationException("CacheSettings section is not configured."); | ||||||
|             } |             } | ||||||
|             services.AddSingleton<ICacheSettings>(cacheSettings); |             services.AddSingleton<ICacheSettings>(cacheSettings); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| namespace Core.Blueprint.Redis | namespace Core.Blueprint.Caching.Contracts | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Interface for managing Redis cache operations. |     /// Interface for managing Redis cache operations. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public interface IRedisCacheProvider |     public interface ICacheProvider | ||||||
|     { |     { | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Retrieves a cache item by its key. |         /// Retrieves a cache item by its key. | ||||||
| @@ -7,12 +7,13 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.0" /> |     <PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.1" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> |     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" /> |     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> |     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> |     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" /> | ||||||
|     <PackageReference Include="StackExchange.Redis" Version="2.8.22" /> |     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" /> | ||||||
|  |     <PackageReference Include="StackExchange.Redis" Version="2.8.37" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
| </Project> | </Project> | ||||||
| @@ -1,11 +1,7 @@ | |||||||
| using System; | using System.Text; | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Linq; |  | ||||||
| using System.Text; |  | ||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
| using System.Threading.Tasks; |  | ||||||
|  |  | ||||||
| namespace Core.Blueprint.Redis.Helpers | namespace Core.Blueprint.Caching.Helpers | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Helper class for generating consistent and normalized cache keys. |     /// Helper class for generating consistent and normalized cache keys. | ||||||
|   | |||||||
							
								
								
									
										86
									
								
								Core.Blueprint.Redis/MemoryCacheProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								Core.Blueprint.Redis/MemoryCacheProvider.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | using Core.Blueprint.Caching.Contracts; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using Microsoft.Extensions.Caching.Memory; | ||||||
|  | using System.Text.Json; | ||||||
|  |  | ||||||
|  | namespace Core.Blueprint.Caching | ||||||
|  | { | ||||||
|  |     public sealed class MemoryCacheProvider : ICacheProvider | ||||||
|  |     { | ||||||
|  |         private readonly IMemoryCache _cache; | ||||||
|  |         private readonly ILogger<MemoryCacheProvider> _logger; | ||||||
|  |         public MemoryCacheProvider(IMemoryCache cache, ILogger<MemoryCacheProvider> logger) | ||||||
|  |         { | ||||||
|  |             _cache = cache; | ||||||
|  |             _logger = logger; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public ValueTask<TEntity> GetAsync<TEntity>(string key) | ||||||
|  |         { | ||||||
|  |             if (_cache.TryGetValue(key, out var value)) | ||||||
|  |             { | ||||||
|  |                 if (value is TEntity typedValue) | ||||||
|  |                 { | ||||||
|  |                     return ValueTask.FromResult(typedValue); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     var json = value?.ToString(); | ||||||
|  |                     var deserialized = JsonSerializer.Deserialize<TEntity>(json); | ||||||
|  |                     return ValueTask.FromResult(deserialized); | ||||||
|  |                 } | ||||||
|  |                 catch (Exception ex) | ||||||
|  |                 { | ||||||
|  |                     _logger.LogWarning(ex, "Error deserializing cache value for key {Key}", key); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return ValueTask.FromResult(default(TEntity)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public ValueTask SetAsync<TEntity>(string key, TEntity value, TimeSpan? expiry = null) | ||||||
|  |         { | ||||||
|  |             var options = new MemoryCacheEntryOptions(); | ||||||
|  |             if (expiry.HasValue) | ||||||
|  |             { | ||||||
|  |                 options.SetAbsoluteExpiration(expiry.Value); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             _cache.Set(key, value, options); | ||||||
|  |             return ValueTask.CompletedTask; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public ValueTask RemoveAsync(string key) | ||||||
|  |         { | ||||||
|  |             _cache.Remove(key); | ||||||
|  |             return ValueTask.CompletedTask; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public ValueTask<bool> ExistsAsync(string key) | ||||||
|  |         { | ||||||
|  |             return ValueTask.FromResult(_cache.TryGetValue(key, out _)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public ValueTask RefreshAsync(string key, TimeSpan? expiry = null) | ||||||
|  |         { | ||||||
|  |             // MemoryCache does not support sliding expiration refresh like Redis, | ||||||
|  |             // so we must re-set the value manually if required. | ||||||
|  |  | ||||||
|  |             if (_cache.TryGetValue(key, out var value)) | ||||||
|  |             { | ||||||
|  |                 _cache.Remove(key); | ||||||
|  |  | ||||||
|  |                 var options = new MemoryCacheEntryOptions(); | ||||||
|  |                 if (expiry.HasValue) | ||||||
|  |                 { | ||||||
|  |                     options.SetAbsoluteExpiration(expiry.Value); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 _cache.Set(key, value, options); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return ValueTask.CompletedTask; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,14 +1,15 @@ | |||||||
| using Azure.Identity; | using Azure.Identity; | ||||||
|  | using Core.Blueprint.Caching.Contracts; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using StackExchange.Redis; | using StackExchange.Redis; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  |  | ||||||
| namespace Core.Blueprint.Redis | namespace Core.Blueprint.Caching | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Redis cache provider for managing cache operations. |     /// Redis cache provider for managing cache operations. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public sealed class RedisCacheProvider : IRedisCacheProvider |     public sealed class RedisCacheProvider : ICacheProvider | ||||||
|     { |     { | ||||||
|         private IDatabase _cacheDatabase = null!; |         private IDatabase _cacheDatabase = null!; | ||||||
|         private readonly ILogger<RedisCacheProvider> _logger; |         private readonly ILogger<RedisCacheProvider> _logger; | ||||||
|   | |||||||
| @@ -11,23 +11,37 @@ namespace Core.Blueprint.Storage.Configuration | |||||||
|     { |     { | ||||||
|         public static IServiceCollection AddBlobStorage(this IServiceCollection services, IConfiguration configuration) |         public static IServiceCollection AddBlobStorage(this IServiceCollection services, IConfiguration configuration) | ||||||
|         { |         { | ||||||
|  |  | ||||||
|             var blobConnection = configuration.GetConnectionString("BlobStorage"); |             var blobConnection = configuration.GetConnectionString("BlobStorage"); | ||||||
|  |  | ||||||
|             if (blobConnection == null || string.IsNullOrWhiteSpace(blobConnection)) |             if (string.IsNullOrWhiteSpace(blobConnection)) | ||||||
|             { |  | ||||||
|                 throw new ArgumentException("The BlobStorage configuration section is missing or empty."); |                 throw new ArgumentException("The BlobStorage configuration section is missing or empty."); | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||||
|  |  | ||||||
|  |             services.AddAzureClients(cfg => | ||||||
|  |             { | ||||||
|  |                 if (environment == "Local") | ||||||
|  |                 { | ||||||
|  |                     var accountKey = configuration.GetSection("BlobStorage:AccountKey").Value; | ||||||
|  |                     var accountName = configuration.GetSection("BlobStorage:AccountName").Value; | ||||||
|  |  | ||||||
|  |                     if(string.IsNullOrEmpty(accountKey) && string.IsNullOrEmpty(accountName)) | ||||||
|  |                         throw new ArgumentException("The BlobStorage configuration section is missing or empty."); | ||||||
|  |  | ||||||
|  |                     cfg.AddBlobServiceClient(configuration.GetConnectionString("BlobStorage")); | ||||||
|  |                 } | ||||||
|  |                 else | ||||||
|  |                 { | ||||||
|                     var chainedCredentials = new ChainedTokenCredential( |                     var chainedCredentials = new ChainedTokenCredential( | ||||||
|                         new ManagedIdentityCredential(), |                         new ManagedIdentityCredential(), | ||||||
|                         new SharedTokenCacheCredential(), |                         new SharedTokenCacheCredential(), | ||||||
|                         new VisualStudioCredential(), |                         new VisualStudioCredential(), | ||||||
|                         new VisualStudioCodeCredential() |                         new VisualStudioCodeCredential() | ||||||
|                     ); |                     ); | ||||||
|             services.AddAzureClients(cfg => |  | ||||||
|             { |                     cfg.AddBlobServiceClient(new Uri(blobConnection)) | ||||||
|                 cfg.AddBlobServiceClient(new Uri(blobConnection)).WithCredential(chainedCredentials); |                         .WithCredential(chainedCredentials); | ||||||
|  |                 } | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             services.AddScoped<IBlobStorageProvider, BlobStorageProvider>(); |             services.AddScoped<IBlobStorageProvider, BlobStorageProvider>(); | ||||||
|   | |||||||
| @@ -162,7 +162,7 @@ namespace Core.Blueprint.Storage.Contracts | |||||||
|         /// </remarks> |         /// </remarks> | ||||||
|         /// <exception cref="ArgumentNullException">Thrown if <paramref name="blobName"/> is null or empty.</exception> |         /// <exception cref="ArgumentNullException">Thrown if <paramref name="blobName"/> is null or empty.</exception> | ||||||
|         /// <exception cref="StorageException">Thrown if there is an issue communicating with the Azure Blob service.</exception> |         /// <exception cref="StorageException">Thrown if there is an issue communicating with the Azure Blob service.</exception> | ||||||
|         BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName); |         ValueTask<BlobDownloadUriAdapter?> GenerateBlobDownloadUri(string blobName); | ||||||
|  |  | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Retrieves the hierarchical folder structure. |         /// Retrieves the hierarchical folder structure. | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using Azure; | using Azure; | ||||||
|  | using Azure.Storage; | ||||||
| using Azure.Storage.Blobs; | using Azure.Storage.Blobs; | ||||||
| using Azure.Storage.Blobs.Models; | using Azure.Storage.Blobs.Models; | ||||||
| using Azure.Storage.Blobs.Specialized; | using Azure.Storage.Blobs.Specialized; | ||||||
| @@ -6,6 +7,7 @@ using Azure.Storage.Sas; | |||||||
| using Core.Blueprint.Storage.Adapters; | using Core.Blueprint.Storage.Adapters; | ||||||
| using Core.Blueprint.Storage.Contracts; | using Core.Blueprint.Storage.Contracts; | ||||||
| using Microsoft.Extensions.Configuration; | using Microsoft.Extensions.Configuration; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
| namespace Core.Blueprint.Storage.Provider | namespace Core.Blueprint.Storage.Provider | ||||||
| { | { | ||||||
| @@ -15,10 +17,12 @@ namespace Core.Blueprint.Storage.Provider | |||||||
|         private readonly BlobContainerClient _blobContainerClient; |         private readonly BlobContainerClient _blobContainerClient; | ||||||
|         private readonly string _containerName; |         private readonly string _containerName; | ||||||
|         private readonly Trie _trie = new Trie(); |         private readonly Trie _trie = new Trie(); | ||||||
|  |         private readonly IConfiguration _configuration; | ||||||
|  |  | ||||||
|         public BlobStorageProvider(BlobServiceClient blobServiceClient, IConfiguration configuration) |         public BlobStorageProvider(BlobServiceClient blobServiceClient, IConfiguration configuration) | ||||||
|         { |         { | ||||||
|             _blobServiceClient = blobServiceClient; |             _blobServiceClient = blobServiceClient; | ||||||
|  |             _configuration = configuration; | ||||||
|             _containerName = configuration.GetSection("BlobStorage:ContainerName").Value ?? ""; |             _containerName = configuration.GetSection("BlobStorage:ContainerName").Value ?? ""; | ||||||
|  |  | ||||||
|             if (string.IsNullOrEmpty(_containerName)) |             if (string.IsNullOrEmpty(_containerName)) | ||||||
| @@ -278,7 +282,8 @@ namespace Core.Blueprint.Storage.Provider | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="blobName">The name of the blob for which the download URI is being generated.</param> |         /// <param name="blobName">The name of the blob for which the download URI is being generated.</param> | ||||||
|         /// <returns> |         /// <returns> | ||||||
|         /// An instance of <see cref="BlobDownloadUriAdapter"/> containing the generated URI, blob name, and status. |         /// An instance of <see cref="BlobDownloadUriAdapter"/> containing the generated URI, blob name, and status, | ||||||
|  |         /// or <c>null</c> if the blob does not exist. | ||||||
|         /// </returns> |         /// </returns> | ||||||
|         /// <remarks> |         /// <remarks> | ||||||
|         /// The generated URI includes a Shared Access Signature (SAS) token, which allows secure, time-limited access to the blob. |         /// The generated URI includes a Shared Access Signature (SAS) token, which allows secure, time-limited access to the blob. | ||||||
| @@ -286,22 +291,36 @@ namespace Core.Blueprint.Storage.Provider | |||||||
|         /// </remarks> |         /// </remarks> | ||||||
|         /// <exception cref="ArgumentNullException">Thrown if <paramref name="blobName"/> is null or empty.</exception> |         /// <exception cref="ArgumentNullException">Thrown if <paramref name="blobName"/> is null or empty.</exception> | ||||||
|         /// <exception cref="StorageException">Thrown if there is an issue communicating with the Azure Blob service.</exception> |         /// <exception cref="StorageException">Thrown if there is an issue communicating with the Azure Blob service.</exception> | ||||||
|         public BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName) |         public async ValueTask<BlobDownloadUriAdapter?> GenerateBlobDownloadUri(string blobName) | ||||||
|         { |         { | ||||||
|             var delegationKey = _blobServiceClient.GetUserDelegationKey(DateTimeOffset.UtcNow, |             if (string.IsNullOrWhiteSpace(blobName)) | ||||||
|                                                                     DateTimeOffset.UtcNow.AddHours(2)); |                 throw new ArgumentNullException(nameof(blobName), "Blob name cannot be null or empty."); | ||||||
|  |  | ||||||
|             var blob = _blobContainerClient.GetBlobClient(blobName); |             var blob = _blobContainerClient.GetBlobClient(blobName); | ||||||
|  |  | ||||||
|             var sasBuilder = new BlobSasBuilder() |             if (!await blob.ExistsAsync()) | ||||||
|  |                 return null; | ||||||
|  |  | ||||||
|  |             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||||
|  |  | ||||||
|  |             if (environment == "Local") | ||||||
|  |             { | ||||||
|  |                 return GenerateDownloadUri(blob); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var delegationKey = await _blobServiceClient.GetUserDelegationKeyAsync( | ||||||
|  |                 DateTimeOffset.UtcNow, | ||||||
|  |                 DateTimeOffset.UtcNow.AddHours(2)); | ||||||
|  |  | ||||||
|  |             var sasBuilder = new BlobSasBuilder | ||||||
|             { |             { | ||||||
|                 BlobContainerName = blob.BlobContainerName, |                 BlobContainerName = blob.BlobContainerName, | ||||||
|                 BlobName = blob.Name, |                 BlobName = blob.Name, | ||||||
|                 Resource = "b", |                 Resource = "b", | ||||||
|                 StartsOn = DateTimeOffset.UtcNow, |                 StartsOn = DateTimeOffset.UtcNow, | ||||||
|                 ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5), |                 ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5) | ||||||
|             }; |             }; | ||||||
|             sasBuilder.SetPermissions(BlobAccountSasPermissions.Read); |             sasBuilder.SetPermissions(BlobSasPermissions.Read); | ||||||
|             sasBuilder.Protocol = SasProtocol.Https; |             sasBuilder.Protocol = SasProtocol.Https; | ||||||
|  |  | ||||||
|             var blobUriBuilder = new BlobUriBuilder(blob.Uri) |             var blobUriBuilder = new BlobUriBuilder(blob.Uri) | ||||||
| @@ -317,6 +336,45 @@ namespace Core.Blueprint.Storage.Provider | |||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Generates a download URI for a blob using a Shared Access Signature in local (Azurite) environment. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="blob">The blob client for which the URI is being generated.</param> | ||||||
|  |         /// <returns>An instance of <see cref="BlobDownloadUriAdapter"/> containing the SAS URI and metadata.</returns> | ||||||
|  |         private BlobDownloadUriAdapter GenerateDownloadUri(BlobClient blob) | ||||||
|  |         { | ||||||
|  |             var sasBuilder = new BlobSasBuilder | ||||||
|  |             { | ||||||
|  |                 BlobContainerName = blob.BlobContainerName, | ||||||
|  |                 BlobName = blob.Name, | ||||||
|  |                 Resource = "b", | ||||||
|  |                 StartsOn = DateTimeOffset.UtcNow, | ||||||
|  |                 ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5) | ||||||
|  |             }; | ||||||
|  |             sasBuilder.SetPermissions(BlobSasPermissions.Read); | ||||||
|  |             sasBuilder.Protocol = SasProtocol.HttpsAndHttp; | ||||||
|  |  | ||||||
|  |             var accountName = _configuration["BlobStorage:AccountName"]; | ||||||
|  |             var accountKey = _configuration["BlobStorage:AccountKey"]; | ||||||
|  |  | ||||||
|  |             var storageCredentials = new StorageSharedKeyCredential(accountName, accountKey); | ||||||
|  |             var sasToken = sasBuilder.ToSasQueryParameters(storageCredentials); | ||||||
|  |  | ||||||
|  |             var blobUriBuilder = new BlobUriBuilder(blob.Uri) | ||||||
|  |             { | ||||||
|  |                 Sas = sasToken | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             return new BlobDownloadUriAdapter | ||||||
|  |             { | ||||||
|  |                 Uri = blobUriBuilder.ToUri(), | ||||||
|  |                 Name = blob.Name, | ||||||
|  |                 Status = "Available" | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Retrieves the hierarchical folder structure. |         /// Retrieves the hierarchical folder structure. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user