Compare commits
	
		
			27 Commits
		
	
	
		
			feature/ad
			...
			developmen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2ede7baae6 | |||
| c42fb5eb00 | |||
| dbc21959eb | |||
| a97e4e2219 | |||
| 35965591f5 | |||
| 38b63455d4 | |||
| fbfa21f89a | |||
| e3cdf1fb32 | |||
| 351cc28181 | |||
| 4e6bf79656 | |||
| 73b909f780 | |||
| 7b326051bb | |||
| ff24c06934 | |||
| 31b26399a9 | |||
|   | 5935e87704 | ||
| 73f9d8550f | |||
| 626105cf0c | |||
| eda79010ce | |||
|   | 852560d0e2 | ||
|   | 4103c4da8d | ||
| a56818bcf8 | |||
|   | 5410a9f9a0 | ||
| 140eab163a | |||
|   | d2a8ced972 | ||
|   | f8c6db55e9 | ||
| 398ca3d7b6 | |||
| ffed92e85c | 
| @@ -16,17 +16,34 @@ namespace Core.Blueprint.KeyVault.Configuration | ||||
|     { | ||||
|         public static IServiceCollection AddKeyVault(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             var keyVaultUriString = configuration["ConnectionStrings:KeyVaultDAL"]; | ||||
|  | ||||
|             if (string.IsNullOrEmpty(keyVaultUriString)) | ||||
|             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||
|  | ||||
|             if(environment ==  "Local") | ||||
|             { | ||||
|                 throw new ArgumentNullException("ConnectionStrings:KeyVault", "KeyVault URI is missing in the configuration."); | ||||
|                 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 keyVaultUri = new Uri(keyVaultUriString); | ||||
|                 if (string.IsNullOrEmpty(keyVaultUriString)) | ||||
|                 { | ||||
|                     throw new ArgumentNullException("ConnectionStrings:KeyVault", "KeyVault URI is missing in the configuration."); | ||||
|                 } | ||||
|  | ||||
|             // Register SecretClient as a singleton | ||||
|             services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); | ||||
|                 var keyVaultUri = new Uri(keyVaultUriString); | ||||
|  | ||||
|                 services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); | ||||
|             } | ||||
|  | ||||
|             services.AddSingleton<IKeyVaultProvider, KeyVaultProvider>(); | ||||
|             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; | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,9 @@ | ||||
|     <PackageReference Include="Azure.Identity" Version="1.13.1" /> | ||||
|     <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.Binder" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="VaultSharp" Version="1.17.5.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -1,93 +1,188 @@ | ||||
| using Azure; | ||||
| using Azure.Security.KeyVault.Secrets; | ||||
| using Azure.Security.KeyVault.Secrets; | ||||
| 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 | ||||
| namespace Core.Blueprint.KeyVault; | ||||
|  | ||||
| /// <summary> | ||||
| /// Provides operations for managing secrets in Azure Key Vault or HashiCorp Vault transparently based on the environment. | ||||
| /// </summary> | ||||
| public sealed class KeyVaultProvider : IKeyVaultProvider | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Provides operations for managing secrets in Azure Key Vault. | ||||
|     /// </summary> | ||||
|     public sealed class KeyVaultProvider(SecretClient keyVaultProvider): IKeyVaultProvider | ||||
|     private readonly string environment; | ||||
|     private readonly SecretClient? azureClient; | ||||
|     private readonly IVaultClient? hashiClient; | ||||
|     private readonly VaultOptions? hashiOptions; | ||||
|  | ||||
|     public KeyVaultProvider(IConfiguration configuration) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Creates a new secret in Azure Key Vault. | ||||
|         /// </summary> | ||||
|         /// <param name="keyVaultRequest">The request containing the name and value of the secret.</param> | ||||
|         /// <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 async ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken) | ||||
|         environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; | ||||
|  | ||||
|         if (environment == "Local") | ||||
|         { | ||||
|             KeyVaultResponse _response = new(); | ||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(keyVaultRequest.Name, keyVaultRequest.Value), cancellationToken); | ||||
|  | ||||
|             _response.Value = azureResponse.Value; | ||||
|             _response.Name = azureResponse.Name; | ||||
|  | ||||
|             return _response; | ||||
|             hashiOptions = configuration.GetSection("Vault").Get<VaultOptions>(); | ||||
|             hashiClient = new VaultClient(new VaultClientSettings( | ||||
|                 hashiOptions?.Address, | ||||
|                 new TokenAuthMethodInfo(hashiOptions?.Token) | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Deletes a secret from Azure Key Vault if it exists. | ||||
|         /// </summary> | ||||
|         /// <param name="secretName">The name of the secret to delete.</param> | ||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||||
|         /// <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) | ||||
|         else | ||||
|         { | ||||
|             var existingSecret = await this.GetSecretAsync(secretName, cancellationToken); | ||||
|             if (existingSecret != null) | ||||
|             { | ||||
|                 await keyVaultProvider.StartDeleteSecretAsync(secretName, cancellationToken); | ||||
|                 return new("Key Deleted", true); | ||||
|             } | ||||
|  | ||||
|             return new("Key Not Found", false); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Retrieves a secret from Azure Key Vault. | ||||
|         /// </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) | ||||
|         { | ||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.GetSecretAsync(secretName, cancellationToken: cancellationToken); | ||||
|  | ||||
|             if (azureResponse == null) | ||||
|             { | ||||
|                 return new(new KeyVaultResponse(), "Key Not Found"); | ||||
|             } | ||||
|  | ||||
|             return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned. | ||||
|         /// </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) | ||||
|         { | ||||
|             KeyVaultResponse _response = new(); | ||||
|             var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken); | ||||
|             if (existingSecret == null) | ||||
|             { | ||||
|                 return new(new KeyVaultResponse(), "Key Not Found"); | ||||
|             } | ||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(newSecret.Name, newSecret.Value), cancellationToken); | ||||
|  | ||||
|             _response.Value = azureResponse.Value; | ||||
|             _response.Name = azureResponse.Name; | ||||
|  | ||||
|             return new(new KeyVaultResponse { Name = newSecret.Name, Value = azureResponse.Value }, string.Empty); | ||||
|             var keyVaultUri = new Uri(configuration["ConnectionStrings:KeyVaultDAL"]!); | ||||
|             azureClient = new SecretClient(keyVaultUri, new Azure.Identity.DefaultAzureCredential()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 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> | ||||
|     /// <param name="secretName">The name of the secret to delete.</param> | ||||
|     /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Tuple"/> containing a status message and a boolean indicating whether the secret was successfully deleted. | ||||
|     /// </returns> | ||||
|     public async ValueTask<(string Message, bool Deleted)> DeleteSecretAsync(string secretName, CancellationToken cancellationToken) | ||||
|     { | ||||
|         if (environment == "Local") | ||||
|         { | ||||
|             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 Not Found", false); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Retrieves a secret from Azure Key Vault or HashiCorp Vault. | ||||
|     /// </summary> | ||||
|     public async ValueTask<(KeyVaultResponse Secret, string? Message)> GetSecretAsync(string secretName, CancellationToken cancellationToken) | ||||
|     { | ||||
|         if (environment == "Local") | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var secret = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretAsync( | ||||
|                     path: secretName, | ||||
|                     mountPoint: hashiOptions!.SecretMount | ||||
|                 ); | ||||
|  | ||||
|                 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"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             KeyVaultSecret azureResponse = await azureClient!.GetSecretAsync(secretName, cancellationToken: cancellationToken); | ||||
|             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> | ||||
|     /// 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<(KeyVaultResponse Secret, string? Message)> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken) | ||||
|     { | ||||
|         var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken); | ||||
|         if (!string.IsNullOrEmpty(existingSecret.Item2)) | ||||
|         { | ||||
|             return new(new KeyVaultResponse(), "Key Not Found"); | ||||
|         } | ||||
|  | ||||
|         var updated = await CreateSecretAsync(newSecret, cancellationToken); | ||||
|         return new(updated, 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."); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Options; | ||||
| using MongoDB.Driver; | ||||
| using static MongoDB.Driver.WriteConcern; | ||||
|  | ||||
| namespace Core.Blueprint.DAL.Mongo.Configuration | ||||
| { | ||||
| @@ -25,40 +26,48 @@ namespace Core.Blueprint.DAL.Mongo.Configuration | ||||
|             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||
|              | ||||
|             services.AddSingleton<IMongoContext, MongoContext>(); | ||||
|             string ConnectionString = configuration.GetSection("ConnectionStrings:MongoDB").Value ?? string.Empty; | ||||
|             string Databasename = configuration.GetSection("MongoDb:DatabaseName").Value ?? string.Empty; | ||||
|             string Audience = string.Empty; | ||||
|  | ||||
|             var ConnectionString = configuration.GetSection("ConnectionStrings:MongoDB").Value ?? string.Empty; | ||||
|             var Databasename = configuration.GetSection("MongoDb:DatabaseName").Value ?? string.Empty; | ||||
|             var Audience = (environment == "Local") | ||||
|                 ? configuration.GetSection("MongoDb:LocalAudience").Value | ||||
|                 : configuration.GetSection("MongoDb:Audience").Value; | ||||
|             if (!environment.Equals("Local", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 Audience = configuration.GetSection("MongoDb:Audience").Value ?? string.Empty; | ||||
|             } | ||||
|  | ||||
|             if (string.IsNullOrEmpty(ConnectionString) || string.IsNullOrEmpty(Databasename) || string.IsNullOrEmpty(Audience)) | ||||
|             if (string.IsNullOrEmpty(ConnectionString) || string.IsNullOrEmpty(Databasename)) | ||||
|             { | ||||
|                 throw new InvalidOperationException("Mongo connection is not configured correctly."); | ||||
|             } | ||||
|  | ||||
|             services.Configure<MongoDbSettings>(options => | ||||
|             services.Configure(delegate (MongoDbSettings options) | ||||
|             { | ||||
|                 options.ConnectionString = ConnectionString; | ||||
|                 options.Databasename = Databasename; | ||||
|                 options.Audience = Audience; | ||||
|             }); | ||||
|  | ||||
|             services.AddSingleton<IMongoClient>(serviceProvider => | ||||
|                 if (!environment.Equals("Local", StringComparison.OrdinalIgnoreCase)) | ||||
|                 { | ||||
|                     options.Audience = Audience; | ||||
|                 } | ||||
|             }); | ||||
|             services.AddSingleton((Func<IServiceProvider, IMongoClient>)delegate (IServiceProvider serviceProvider) | ||||
|             { | ||||
|                 var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value; | ||||
|                 var mongoClientSettings = MongoClientSettings.FromConnectionString(settings.ConnectionString); | ||||
|                 mongoClientSettings.Credential = MongoCredential.CreateOidcCredential(new AzureIdentityProvider(settings.Audience)); | ||||
|                 MongoDbSettings value2 = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value; | ||||
|                 MongoClientSettings mongoClientSettings = MongoClientSettings.FromConnectionString(value2.ConnectionString); | ||||
|  | ||||
|                 if (!environment.Equals("Local", StringComparison.OrdinalIgnoreCase))  | ||||
|                 { | ||||
|                     mongoClientSettings.Credential = MongoCredential.CreateOidcCredential(new AzureIdentityProvider(value2.Audience)); | ||||
|                 } | ||||
|                      | ||||
|                 return new MongoClient(mongoClientSettings); | ||||
|             }); | ||||
|  | ||||
|             services.AddSingleton<IMongoDatabase>(serviceProvider => | ||||
|             services.AddSingleton(delegate (IServiceProvider serviceProvider) | ||||
|             { | ||||
|                 var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value; | ||||
|                 var client = serviceProvider.GetRequiredService<IMongoClient>(); | ||||
|                 return client.GetDatabase(settings.Databasename); | ||||
|                 MongoDbSettings value = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value; | ||||
|                 return serviceProvider.GetRequiredService<IMongoClient>().GetDatabase(value.Databasename); | ||||
|             }); | ||||
|  | ||||
|             services.AddSingleton<IMongoDbSettings>(serviceProvider => serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value); | ||||
|  | ||||
|             services.AddSingleton((Func<IServiceProvider, IMongoDbSettings>)((IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value)); | ||||
|             return services; | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										12
									
								
								Core.Blueprint.Mongo/nuget.config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Core.Blueprint.Mongo/nuget.config
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <configuration> | ||||
| 	<packageSources> | ||||
| 		<add key="Gitea" value="https://gitea.white-enciso.pro/api/packages/AgileWebs/nuget" /> | ||||
| 	</packageSources> | ||||
| 	<packageSourceCredentials> | ||||
| 		<Gitea> | ||||
| 			<Username>oscarmmtz</Username> | ||||
| 			<ClearTextPassword>544831e1ceaf52958e02c5de4d23cbde9e7a860a</ClearTextPassword> | ||||
| 		</Gitea> | ||||
| 	</packageSourceCredentials> | ||||
| </configuration> | ||||
| @@ -1,10 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Core.Blueprint.Redis | ||||
| namespace Core.Blueprint.Redis | ||||
| { | ||||
|     public interface ICacheSettings | ||||
|     { | ||||
|   | ||||
| @@ -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>(); | ||||
|   | ||||
| @@ -4,15 +4,16 @@ | ||||
|     <TargetFramework>net8.0</TargetFramework> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|     <Nullable>enable</Nullable> | ||||
| 	<PackageId>Core.Blueprint.Redis</PackageId> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="StackExchange.Redis" Version="2.8.22" /> | ||||
|     <PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.1" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" /> | ||||
|     <PackageReference Include="StackExchange.Redis" Version="2.8.37" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -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,34 +21,52 @@ 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}") | ||||
|                     .ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential()); | ||||
|                 if (_useRedis) | ||||
|                 { | ||||
|                     var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||
|                     ConnectionMultiplexer connectionMultiplexer; | ||||
|  | ||||
|                 configurationOptions.AbortOnConnectFail = false; | ||||
|                 var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions); | ||||
|                     if (environment.Equals("Local", StringComparison.OrdinalIgnoreCase)) | ||||
|                     { | ||||
|                         connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(connectionString); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         var configurationOptions = await ConfigurationOptions.Parse(connectionString) | ||||
|                             .ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential()); | ||||
|  | ||||
|                 _logger.LogInformation("Successfully connected to Redis."); | ||||
|                         configurationOptions.AbortOnConnectFail = false; | ||||
|  | ||||
|                 return connectionMultiplexer.GetDatabase(); | ||||
|                         connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions); | ||||
|                     } | ||||
|  | ||||
|                     _logger.LogInformation("Successfully connected to Redis."); | ||||
|  | ||||
|                     return connectionMultiplexer.GetDatabase(); | ||||
|                 } | ||||
|  | ||||
|                 return null; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
| @@ -65,15 +85,21 @@ namespace Core.Blueprint.Redis | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var value = await _cacheDatabase.StringGetAsync(key); | ||||
|                 if (value.IsNullOrEmpty) | ||||
|                 if (_useRedis is not false) | ||||
|                 { | ||||
|                     _logger.LogInformation($"Cache miss for key: {key}"); | ||||
|                     return default; | ||||
|                     var value = await _cacheDatabase.StringGetAsync(key); | ||||
|  | ||||
|                     if (value.IsNullOrEmpty) | ||||
|                     { | ||||
|                         _logger.LogInformation($"Cache miss for key: {key}"); | ||||
|                         return default; | ||||
|                     } | ||||
|  | ||||
|                     _logger.LogInformation($"Cache hit for key: {key}"); | ||||
|                     return JsonSerializer.Deserialize<TEntity>(value); | ||||
|                 } | ||||
|  | ||||
|                 _logger.LogInformation($"Cache hit for key: {key}"); | ||||
|                 return JsonSerializer.Deserialize<TEntity>(value); | ||||
|                 return default; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
| @@ -92,9 +118,12 @@ namespace Core.Blueprint.Redis | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var json = JsonSerializer.Serialize(value); | ||||
|                 await _cacheDatabase.StringSetAsync(key, json, expiry); | ||||
|                 _logger.LogInformation($"Cache item set with key: {key}"); | ||||
|                 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) | ||||
|             { | ||||
| @@ -111,8 +140,11 @@ namespace Core.Blueprint.Redis | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 await _cacheDatabase.KeyDeleteAsync(key); | ||||
|                 _logger.LogInformation($"Cache item removed with key: {key}"); | ||||
|                 if (_useRedis is not false) | ||||
|                 { | ||||
|                     await _cacheDatabase.KeyDeleteAsync(key); | ||||
|                     _logger.LogInformation($"Cache item removed with key: {key}"); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
| @@ -130,9 +162,13 @@ namespace Core.Blueprint.Redis | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var exists = await _cacheDatabase.KeyExistsAsync(key); | ||||
|                 _logger.LogInformation($"Cache item exists check for key: {key} - {exists}"); | ||||
|                 return exists; | ||||
|                 if (_useRedis is not false) | ||||
|                 { | ||||
|                     var exists = await _cacheDatabase.KeyExistsAsync(key); | ||||
|                     _logger.LogInformation($"Cache item exists check for key: {key} - {exists}"); | ||||
|                 } | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
| @@ -150,15 +186,18 @@ namespace Core.Blueprint.Redis | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var value = await _cacheDatabase.StringGetAsync(key); | ||||
|                 if (!value.IsNullOrEmpty) | ||||
|                 if (_useRedis is not false) | ||||
|                 { | ||||
|                     await _cacheDatabase.StringSetAsync(key, value, expiry); | ||||
|                     _logger.LogInformation($"Cache item refreshed with key: {key}"); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     _logger.LogWarning($"Cache item with key: {key} does not exist, cannot refresh"); | ||||
|                     var value = await _cacheDatabase.StringGetAsync(key); | ||||
|                     if (!value.IsNullOrEmpty) | ||||
|                     { | ||||
|                         await _cacheDatabase.StringSetAsync(key, value, expiry); | ||||
|                         _logger.LogInformation($"Cache item refreshed with key: {key}"); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         _logger.LogWarning($"Cache item with key: {key} does not exist, cannot refresh"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|   | ||||
| @@ -18,7 +18,9 @@ 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 chainedCredentials = new ChainedTokenCredential( | ||||
|             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||
|  | ||||
|                 var chainedCredentials = new ChainedTokenCredential( | ||||
|                     new ManagedIdentityCredential(), | ||||
|                     new SharedTokenCacheCredential(), | ||||
|                     new VisualStudioCredential(), | ||||
|   | ||||
| @@ -11,23 +11,37 @@ namespace Core.Blueprint.Storage.Configuration | ||||
|     { | ||||
|         public static IServiceCollection AddBlobStorage(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|  | ||||
|             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."); | ||||
|             } | ||||
|  | ||||
|             var chainedCredentials = new ChainedTokenCredential( | ||||
|                     new ManagedIdentityCredential(), | ||||
|                     new SharedTokenCacheCredential(), | ||||
|                     new VisualStudioCredential(), | ||||
|                     new VisualStudioCodeCredential() | ||||
|                 ); | ||||
|             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||
|  | ||||
|             services.AddAzureClients(cfg => | ||||
|             { | ||||
|                 cfg.AddBlobServiceClient(new Uri(blobConnection)).WithCredential(chainedCredentials); | ||||
|                 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( | ||||
|                         new ManagedIdentityCredential(), | ||||
|                         new SharedTokenCacheCredential(), | ||||
|                         new VisualStudioCredential(), | ||||
|                         new VisualStudioCodeCredential() | ||||
|                     ); | ||||
|  | ||||
|                     cfg.AddBlobServiceClient(new Uri(blobConnection)) | ||||
|                         .WithCredential(chainedCredentials); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             services.AddScoped<IBlobStorageProvider, BlobStorageProvider>(); | ||||
|   | ||||
| @@ -162,7 +162,7 @@ namespace Core.Blueprint.Storage.Contracts | ||||
|         /// </remarks> | ||||
|         /// <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> | ||||
|         BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName); | ||||
|         ValueTask<BlobDownloadUriAdapter?> GenerateBlobDownloadUri(string blobName); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Retrieves the hierarchical folder structure. | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Azure; | ||||
| using Azure.Storage; | ||||
| using Azure.Storage.Blobs; | ||||
| using Azure.Storage.Blobs.Models; | ||||
| using Azure.Storage.Blobs.Specialized; | ||||
| @@ -6,6 +7,7 @@ using Azure.Storage.Sas; | ||||
| using Core.Blueprint.Storage.Adapters; | ||||
| using Core.Blueprint.Storage.Contracts; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Core.Blueprint.Storage.Provider | ||||
| { | ||||
| @@ -15,10 +17,12 @@ namespace Core.Blueprint.Storage.Provider | ||||
|         private readonly BlobContainerClient _blobContainerClient; | ||||
|         private readonly string _containerName; | ||||
|         private readonly Trie _trie = new Trie(); | ||||
|         private readonly IConfiguration _configuration; | ||||
|  | ||||
|         public BlobStorageProvider(BlobServiceClient blobServiceClient, IConfiguration configuration) | ||||
|         { | ||||
|             _blobServiceClient = blobServiceClient; | ||||
|             _configuration = configuration; | ||||
|             _containerName = configuration.GetSection("BlobStorage:ContainerName").Value ?? ""; | ||||
|  | ||||
|             if (string.IsNullOrEmpty(_containerName)) | ||||
| @@ -278,7 +282,8 @@ namespace Core.Blueprint.Storage.Provider | ||||
|         /// </summary> | ||||
|         /// <param name="blobName">The name of the blob for which the download URI is being generated.</param> | ||||
|         /// <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> | ||||
|         /// <remarks> | ||||
|         /// 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> | ||||
|         /// <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> | ||||
|         public BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName) | ||||
|         public async ValueTask<BlobDownloadUriAdapter?> GenerateBlobDownloadUri(string blobName) | ||||
|         { | ||||
|             var delegationKey = _blobServiceClient.GetUserDelegationKey(DateTimeOffset.UtcNow, | ||||
|                                                                     DateTimeOffset.UtcNow.AddHours(2)); | ||||
|             if (string.IsNullOrWhiteSpace(blobName)) | ||||
|                 throw new ArgumentNullException(nameof(blobName), "Blob name cannot be null or empty."); | ||||
|  | ||||
|             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, | ||||
|                 BlobName = blob.Name, | ||||
|                 Resource = "b", | ||||
|                 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; | ||||
|  | ||||
|             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> | ||||
|         /// Retrieves the hierarchical folder structure. | ||||
|         /// </summary> | ||||
|   | ||||
							
								
								
									
										9
									
								
								nuget.config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								nuget.config
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <configuration> | ||||
|   <packageSources> | ||||
|     <!-- Tu BaGet primero --> | ||||
|     <add key="BaGet" value="https://nuget.dream-views.com/v3/index.json" protocolVersion="3" /> | ||||
|     <!-- NuGet oficial como fallback (si quieres) --> | ||||
|     <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> | ||||
|   </packageSources> | ||||
| </configuration> | ||||
		Reference in New Issue
	
	Block a user