From 83fc1878c4a3c5cfeccd468d08add8ed7fee26af Mon Sep 17 00:00:00 2001 From: Sergio Matias Urquin Date: Tue, 29 Apr 2025 18:42:29 -0600 Subject: [PATCH] Add project files. --- Core.BluePrint.Packages.sln | 55 +++ .../Adapters/KeyVaultRequest.cs | 9 + .../Adapters/KeyVaultResponse.cs | 10 + .../Configuration/RegisterBlueprint.cs | 35 ++ .../Contracts/IKeyVaultProvider.cs | 48 +++ .../Core.Blueprint.KeyVault.csproj | 16 + .../Provider/KeyVaultProvider.cs | 93 ++++ .../Adapters/ErrorDetails.cs | 43 ++ Core.Blueprint.Logging/Adapters/HttpError.cs | 45 ++ .../Adapters/HttpException.cs | 41 ++ Core.Blueprint.Logging/Adapters/LogDetail.cs | 120 ++++++ .../Adapters/LogOperation.cs | 55 +++ .../Adapters/LogSeverity.cs | 41 ++ Core.Blueprint.Logging/Adapters/LogTarget.cs | 41 ++ .../Adapters/ServiceSettings.cs | 20 + .../Configuration/Registerblueprint.cs | 70 +++ Core.Blueprint.Logging/Constants/Claims.cs | 48 +++ .../Constants/DisplayNames.cs | 397 ++++++++++++++++++ .../Constants/EnvironmentVariables.cs | 21 + .../Constants/ErrorCodes.cs | 79 ++++ Core.Blueprint.Logging/Constants/Headers.cs | 10 + Core.Blueprint.Logging/Constants/MimeTypes.cs | 148 +++++++ Core.Blueprint.Logging/Constants/Responses.cs | 29 ++ .../Contracts/ILoggerProvider.cs | 12 + .../Core.Blueprint.Logging.csproj | 21 + .../Middleware/HttpErrorMiddleware.cs | 92 ++++ .../Middleware/HttpLogger.cs | 237 +++++++++++ .../Middleware/HttpLoggingMiddleware.cs | 69 +++ .../Provider/LoggerProvider.cs | 89 ++++ .../Attributes/CollectionAttributeName.cs | 24 ++ .../IdentityProvider/HeathIdentityProvider.cs | 121 ++++++ .../Configuration/RegisterBlueprint.cs | 65 +++ Core.Blueprint.Mongo/Context/MongoContext.cs | 66 +++ .../Context/MongoDbSettings.cs | 25 ++ .../Contracts/ICollectionRepository.cs | 152 +++++++ Core.Blueprint.Mongo/Contracts/IDocument.cs | 39 ++ .../Contracts/IMongoContext.cs | 45 ++ .../Contracts/IMongoDbSettings.cs | 32 ++ .../Core.Blueprint.Mongo.csproj | 19 + Core.Blueprint.Mongo/Entities/Document.cs | 81 ++++ Core.Blueprint.Mongo/Entities/StatusEnum.cs | 28 ++ .../Provider/MongoProvider.cs | 40 ++ .../Repositories/CollectionRepository.cs | 252 +++++++++++ .../Adapters/CacheSettings.cs | 23 + .../Configuration/RegisterBlueprint.cs | 43 ++ .../Contracts/IRedisCacheProvider.cs | 48 +++ .../Core.Blueprint.Redis.csproj | 18 + .../Helpers/RedisCacheKeyHelper.cs | 63 +++ Core.Blueprint.Redis/RedisCacheProvider.cs | 171 ++++++++ .../Adapters/BaseSQLAdapter.cs | 65 +++ .../Adapters/StatusEnum.cs | 29 ++ .../Configuration/RegisterBlueprint.cs | 33 ++ .../Contracts/IBaseSQLAdapter.cs | 57 +++ .../Contracts/IEntityRepository.cs | 108 +++++ .../Core.Blueprint.SQLServer.csproj | 16 + .../Repositories/EntityRepository.cs | 182 ++++++++ Core.Blueprint.Storage/Adapters/BlobAddDto.cs | 8 + .../Adapters/BlobDownloadAdapter.cs | 12 + .../Adapters/BlobDownloadUriAdapter.cs | 9 + .../Adapters/BlobFileAdapter.cs | 13 + .../Adapters/BlobStorageAdapter.cs | 15 + .../Adapters/BlobStorageFolder.cs | 16 + .../Adapters/Trie/TrieNode.cs | 66 +++ .../Configuration/RegisterBlueprint.cs | 38 ++ .../Contracts/IBlobStorageProvider.cs | 181 ++++++++ .../Core.Blueprint.Storage.csproj | 17 + .../Provider/BlobStorageProvider.cs | 372 ++++++++++++++++ 67 files changed, 4586 insertions(+) create mode 100644 Core.BluePrint.Packages.sln create mode 100644 Core.Blueprint.KeyVault/Adapters/KeyVaultRequest.cs create mode 100644 Core.Blueprint.KeyVault/Adapters/KeyVaultResponse.cs create mode 100644 Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs create mode 100644 Core.Blueprint.KeyVault/Contracts/IKeyVaultProvider.cs create mode 100644 Core.Blueprint.KeyVault/Core.Blueprint.KeyVault.csproj create mode 100644 Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs create mode 100644 Core.Blueprint.Logging/Adapters/ErrorDetails.cs create mode 100644 Core.Blueprint.Logging/Adapters/HttpError.cs create mode 100644 Core.Blueprint.Logging/Adapters/HttpException.cs create mode 100644 Core.Blueprint.Logging/Adapters/LogDetail.cs create mode 100644 Core.Blueprint.Logging/Adapters/LogOperation.cs create mode 100644 Core.Blueprint.Logging/Adapters/LogSeverity.cs create mode 100644 Core.Blueprint.Logging/Adapters/LogTarget.cs create mode 100644 Core.Blueprint.Logging/Adapters/ServiceSettings.cs create mode 100644 Core.Blueprint.Logging/Configuration/Registerblueprint.cs create mode 100644 Core.Blueprint.Logging/Constants/Claims.cs create mode 100644 Core.Blueprint.Logging/Constants/DisplayNames.cs create mode 100644 Core.Blueprint.Logging/Constants/EnvironmentVariables.cs create mode 100644 Core.Blueprint.Logging/Constants/ErrorCodes.cs create mode 100644 Core.Blueprint.Logging/Constants/Headers.cs create mode 100644 Core.Blueprint.Logging/Constants/MimeTypes.cs create mode 100644 Core.Blueprint.Logging/Constants/Responses.cs create mode 100644 Core.Blueprint.Logging/Contracts/ILoggerProvider.cs create mode 100644 Core.Blueprint.Logging/Core.Blueprint.Logging.csproj create mode 100644 Core.Blueprint.Logging/Middleware/HttpErrorMiddleware.cs create mode 100644 Core.Blueprint.Logging/Middleware/HttpLogger.cs create mode 100644 Core.Blueprint.Logging/Middleware/HttpLoggingMiddleware.cs create mode 100644 Core.Blueprint.Logging/Provider/LoggerProvider.cs create mode 100644 Core.Blueprint.Mongo/Attributes/CollectionAttributeName.cs create mode 100644 Core.Blueprint.Mongo/Configuration/IdentityProvider/HeathIdentityProvider.cs create mode 100644 Core.Blueprint.Mongo/Configuration/RegisterBlueprint.cs create mode 100644 Core.Blueprint.Mongo/Context/MongoContext.cs create mode 100644 Core.Blueprint.Mongo/Context/MongoDbSettings.cs create mode 100644 Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs create mode 100644 Core.Blueprint.Mongo/Contracts/IDocument.cs create mode 100644 Core.Blueprint.Mongo/Contracts/IMongoContext.cs create mode 100644 Core.Blueprint.Mongo/Contracts/IMongoDbSettings.cs create mode 100644 Core.Blueprint.Mongo/Core.Blueprint.Mongo.csproj create mode 100644 Core.Blueprint.Mongo/Entities/Document.cs create mode 100644 Core.Blueprint.Mongo/Entities/StatusEnum.cs create mode 100644 Core.Blueprint.Mongo/Provider/MongoProvider.cs create mode 100644 Core.Blueprint.Mongo/Repositories/CollectionRepository.cs create mode 100644 Core.Blueprint.Redis/Adapters/CacheSettings.cs create mode 100644 Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs create mode 100644 Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs create mode 100644 Core.Blueprint.Redis/Core.Blueprint.Redis.csproj create mode 100644 Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs create mode 100644 Core.Blueprint.Redis/RedisCacheProvider.cs create mode 100644 Core.Blueprint.SQLServer/Adapters/BaseSQLAdapter.cs create mode 100644 Core.Blueprint.SQLServer/Adapters/StatusEnum.cs create mode 100644 Core.Blueprint.SQLServer/Configuration/RegisterBlueprint.cs create mode 100644 Core.Blueprint.SQLServer/Contracts/IBaseSQLAdapter.cs create mode 100644 Core.Blueprint.SQLServer/Contracts/IEntityRepository.cs create mode 100644 Core.Blueprint.SQLServer/Core.Blueprint.SQLServer.csproj create mode 100644 Core.Blueprint.SQLServer/Repositories/EntityRepository.cs create mode 100644 Core.Blueprint.Storage/Adapters/BlobAddDto.cs create mode 100644 Core.Blueprint.Storage/Adapters/BlobDownloadAdapter.cs create mode 100644 Core.Blueprint.Storage/Adapters/BlobDownloadUriAdapter.cs create mode 100644 Core.Blueprint.Storage/Adapters/BlobFileAdapter.cs create mode 100644 Core.Blueprint.Storage/Adapters/BlobStorageAdapter.cs create mode 100644 Core.Blueprint.Storage/Adapters/BlobStorageFolder.cs create mode 100644 Core.Blueprint.Storage/Adapters/Trie/TrieNode.cs create mode 100644 Core.Blueprint.Storage/Configuration/RegisterBlueprint.cs create mode 100644 Core.Blueprint.Storage/Contracts/IBlobStorageProvider.cs create mode 100644 Core.Blueprint.Storage/Core.Blueprint.Storage.csproj create mode 100644 Core.Blueprint.Storage/Provider/BlobStorageProvider.cs diff --git a/Core.BluePrint.Packages.sln b/Core.BluePrint.Packages.sln new file mode 100644 index 0000000..7a136ec --- /dev/null +++ b/Core.BluePrint.Packages.sln @@ -0,0 +1,55 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Blueprint.KeyVault", "Core.Blueprint.KeyVault\Core.Blueprint.KeyVault.csproj", "{0B4D475C-6A41-443C-8FB4-21C759EDCE63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Mongo", "Core.Blueprint.Mongo\Core.Blueprint.Mongo.csproj", "{27A8E3E1-D613-4D5B-8105-485699409F1E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Redis", "Core.Blueprint.Redis\Core.Blueprint.Redis.csproj", "{11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Storage", "Core.Blueprint.Storage\Core.Blueprint.Storage.csproj", "{636E4520-79F9-46C8-990D-08F2D24A151C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.SQLServer", "Core.Blueprint.SQLServer\Core.Blueprint.SQLServer.csproj", "{A9CC126A-C187-49EF-9784-0F9F5B8ABDB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Logging", "Core.Blueprint.Logging\Core.Blueprint.Logging.csproj", "{85B4BC7C-5800-40A9-8310-F4EB2C82AF39}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0B4D475C-6A41-443C-8FB4-21C759EDCE63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B4D475C-6A41-443C-8FB4-21C759EDCE63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B4D475C-6A41-443C-8FB4-21C759EDCE63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B4D475C-6A41-443C-8FB4-21C759EDCE63}.Release|Any CPU.Build.0 = Release|Any CPU + {27A8E3E1-D613-4D5B-8105-485699409F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27A8E3E1-D613-4D5B-8105-485699409F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27A8E3E1-D613-4D5B-8105-485699409F1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27A8E3E1-D613-4D5B-8105-485699409F1E}.Release|Any CPU.Build.0 = Release|Any CPU + {11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}.Release|Any CPU.Build.0 = Release|Any CPU + {636E4520-79F9-46C8-990D-08F2D24A151C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {636E4520-79F9-46C8-990D-08F2D24A151C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {636E4520-79F9-46C8-990D-08F2D24A151C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {636E4520-79F9-46C8-990D-08F2D24A151C}.Release|Any CPU.Build.0 = Release|Any CPU + {A9CC126A-C187-49EF-9784-0F9F5B8ABDB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9CC126A-C187-49EF-9784-0F9F5B8ABDB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9CC126A-C187-49EF-9784-0F9F5B8ABDB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9CC126A-C187-49EF-9784-0F9F5B8ABDB1}.Release|Any CPU.Build.0 = Release|Any CPU + {85B4BC7C-5800-40A9-8310-F4EB2C82AF39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85B4BC7C-5800-40A9-8310-F4EB2C82AF39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85B4BC7C-5800-40A9-8310-F4EB2C82AF39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85B4BC7C-5800-40A9-8310-F4EB2C82AF39}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60FDC812-CC26-4C4A-BCA0-90603A77E99D} + EndGlobalSection +EndGlobal diff --git a/Core.Blueprint.KeyVault/Adapters/KeyVaultRequest.cs b/Core.Blueprint.KeyVault/Adapters/KeyVaultRequest.cs new file mode 100644 index 0000000..4d0a72f --- /dev/null +++ b/Core.Blueprint.KeyVault/Adapters/KeyVaultRequest.cs @@ -0,0 +1,9 @@ + +namespace Core.Blueprint.KeyVault +{ + public sealed class KeyVaultRequest + { + public required string Name { get; set; } + public required string Value { get; set; } + } +} diff --git a/Core.Blueprint.KeyVault/Adapters/KeyVaultResponse.cs b/Core.Blueprint.KeyVault/Adapters/KeyVaultResponse.cs new file mode 100644 index 0000000..2811656 --- /dev/null +++ b/Core.Blueprint.KeyVault/Adapters/KeyVaultResponse.cs @@ -0,0 +1,10 @@ + + +namespace Core.Blueprint.KeyVault +{ + public sealed class KeyVaultResponse + { + public string Name { get; set; } = null!; + public string Value { get; set; } = null!; + } +} diff --git a/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs b/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs new file mode 100644 index 0000000..ea094e9 --- /dev/null +++ b/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs @@ -0,0 +1,35 @@ +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Core.Blueprint.KeyVault.Configuration +{ + /// + /// Registers the SecretClient for Azure Key Vault as a singleton service. + /// + /// The IServiceCollection to add the services to. + /// The application's configuration. + /// The updated IServiceCollection. + /// Thrown when the KeyVault URI is missing in the configuration. + public static class RegisterBlueprint + { + public static IServiceCollection AddKeyVault(this IServiceCollection services, IConfiguration configuration) + { + var keyVaultUriString = configuration["ConnectionStrings:KeyVaultDAL"]; + + if (string.IsNullOrEmpty(keyVaultUriString)) + { + throw new ArgumentNullException("ConnectionStrings:KeyVault", "KeyVault URI is missing in the configuration."); + } + + var keyVaultUri = new Uri(keyVaultUriString); + + // Register SecretClient as a singleton + services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); + + services.AddSingleton(); + return services; + } + } +} diff --git a/Core.Blueprint.KeyVault/Contracts/IKeyVaultProvider.cs b/Core.Blueprint.KeyVault/Contracts/IKeyVaultProvider.cs new file mode 100644 index 0000000..b0025c6 --- /dev/null +++ b/Core.Blueprint.KeyVault/Contracts/IKeyVaultProvider.cs @@ -0,0 +1,48 @@ + +namespace Core.Blueprint.KeyVault +{ + /// + /// Interface for managing secrets in Azure Key Vault. + /// + public interface IKeyVaultProvider + { + /// + /// Creates a new secret in Azure Key Vault. + /// + /// The request containing the name and value of the secret. + /// The cancellation token to cancel the operation. + /// A containing the details of the created secret. + ValueTask CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken); + + /// + /// Deletes a secret from Azure Key Vault if it exists. + /// + /// The name of the secret to delete. + /// The cancellation token to cancel the operation. + /// + /// A containing a status message and a boolean indicating whether the secret was successfully deleted. + /// + ValueTask> DeleteSecretAsync(string secretName, CancellationToken cancellationToken); + + /// + /// Retrieves a secret from Azure Key Vault. + /// + /// The name of the secret to retrieve. + /// The cancellation token to cancel the operation. + /// + /// A containing the with secret details + /// and an optional error message if the secret was not found. + /// + ValueTask> GetSecretAsync(string secretName, CancellationToken cancellationToken); + + /// + /// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned. + /// + /// The updated secret information. + /// The cancellation token to cancel the operation. + /// + /// A containing the updated and an optional error message if the secret was not found. + /// + ValueTask> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken); + } +} diff --git a/Core.Blueprint.KeyVault/Core.Blueprint.KeyVault.csproj b/Core.Blueprint.KeyVault/Core.Blueprint.KeyVault.csproj new file mode 100644 index 0000000..84d4a87 --- /dev/null +++ b/Core.Blueprint.KeyVault/Core.Blueprint.KeyVault.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs b/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs new file mode 100644 index 0000000..707fb5c --- /dev/null +++ b/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs @@ -0,0 +1,93 @@ +using Azure; +using Azure.Security.KeyVault.Secrets; + +namespace Core.Blueprint.KeyVault +{ + /// + /// Provides operations for managing secrets in Azure Key Vault. + /// + public sealed class KeyVaultProvider(SecretClient keyVaultProvider): IKeyVaultProvider + { + /// + /// Creates a new secret in Azure Key Vault. + /// + /// The request containing the name and value of the secret. + /// The cancellation token to cancel the operation. + /// A containing the details of the created secret. + public async ValueTask CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken) + { + 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; + } + + /// + /// Deletes a secret from Azure Key Vault if it exists. + /// + /// The name of the secret to delete. + /// The cancellation token to cancel the operation. + /// + /// A containing a status message and a boolean indicating whether the secret was successfully deleted. + /// + public async ValueTask> DeleteSecretAsync(string secretName, CancellationToken cancellationToken) + { + 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); + } + + /// + /// Retrieves a secret from Azure Key Vault. + /// + /// The name of the secret to retrieve. + /// The cancellation token to cancel the operation. + /// + /// A containing the with secret details + /// and an optional error message if the secret was not found. + /// + public async ValueTask> 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); + } + + /// + /// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned. + /// + /// The updated secret information. + /// The cancellation token to cancel the operation. + /// + /// A containing the updated and an optional error message if the secret was not found. + /// + public async ValueTask> 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); + } + } +} diff --git a/Core.Blueprint.Logging/Adapters/ErrorDetails.cs b/Core.Blueprint.Logging/Adapters/ErrorDetails.cs new file mode 100644 index 0000000..77ebbdf --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/ErrorDetails.cs @@ -0,0 +1,43 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// The service error details transfer object. + /// + public class ErrorDetails + { + /// + /// Gets or sets the service error code. + /// + /// healthy + [DisplayName(DisplayNames.ErrorCode)] + [JsonPropertyName(DisplayNames.ErrorCode)] + public string? ErrorCode { get; set; } + + /// + /// Gets or sets the service error message. + /// + /// This is an example message. + [DisplayName(DisplayNames.Message)] + [JsonPropertyName(DisplayNames.Message)] + public string? Message { get; set; } + + /// + /// Gets or sets the service target. + /// + /// healthy + [DisplayName(DisplayNames.Target)] + [JsonPropertyName(DisplayNames.Target)] + public string? Target { get; set; } + } +} + + diff --git a/Core.Blueprint.Logging/Adapters/HttpError.cs b/Core.Blueprint.Logging/Adapters/HttpError.cs new file mode 100644 index 0000000..21eea87 --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/HttpError.cs @@ -0,0 +1,45 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// The service HTTP error data transfer object. + /// + public class HttpError + { + /// + /// Gets or sets the error. + /// + [DisplayName(DisplayNames.Error)] + [JsonPropertyName(DisplayNames.Error)] + public ErrorDetails Error { get; set; } + + /// + /// Creates a new instance of + /// with custom parameters. + /// + /// The HTTP error message. + /// The HTTP error code. + /// The HTTP error target. + public HttpError( + string? message, + string? errorCode, + string? target) + { + Error = new ErrorDetails + { + ErrorCode = errorCode, + Message = message, + Target = target, + }; + } + } +} + diff --git a/Core.Blueprint.Logging/Adapters/HttpException.cs b/Core.Blueprint.Logging/Adapters/HttpException.cs new file mode 100644 index 0000000..a6cc11d --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/HttpException.cs @@ -0,0 +1,41 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Blueprint.Logging +{ + /// + /// The service HTTP exception. + /// Extends the class. + /// + public class HttpException : Exception + { + /// + /// Gets or sets the exception error code. + /// + public string? ErrorCode { get; set; } + + /// + /// Gets or sets the exception status code. + /// + public int StatusCode { get; set; } + + /// + /// Creates a new instance of . + /// + /// The exception status code. + /// The exception error code. + /// The exception message. + public HttpException( + int statusCode, + string errorCode, + string message) + : base(message) + { + ErrorCode = errorCode; + StatusCode = statusCode; + } + } +} diff --git a/Core.Blueprint.Logging/Adapters/LogDetail.cs b/Core.Blueprint.Logging/Adapters/LogDetail.cs new file mode 100644 index 0000000..b73477f --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/LogDetail.cs @@ -0,0 +1,120 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// The service logger detail object. + /// + /// The generic message type. + public class LogDetail + { + /// + /// Gets or sets the log severity. + /// + /// info + [DisplayName(DisplayNames.Severity)] + [JsonPropertyName(DisplayNames.Severity)] + public LogSeverity Severity { get; set; } + + /// + /// Gets or sets the timestamp. + /// + [DisplayName(DisplayNames.Timestamp)] + [JsonPropertyName(DisplayNames.Timestamp)] + public DateTime Timestamp { get; set; } + + /// + /// Gets or sets the environment. + /// + /// Development + [DisplayName(DisplayNames.Environment)] + [JsonPropertyName(DisplayNames.Environment)] + public string? Environment { get; set; } + + /// + /// Gets or sets the target. + /// + [DisplayName(DisplayNames.Target)] + [JsonPropertyName(DisplayNames.Target)] + public LogTarget? Target { get; set; } + + /// + /// Gets or sets the x-forwarded-for header. + /// + /// localhost + [DisplayName(DisplayNames.XForwardedFor)] + [JsonPropertyName(DisplayNames.XForwardedFor)] + public string? XForwardedFor { get; set; } + + /// + /// Gets or sets the service identifier. + /// + /// + [DisplayName(DisplayNames.ServiceId)] + [JsonPropertyName(DisplayNames.ServiceId)] + public string? ServiceId { get; set; } + + /// + /// Gets or sets the request identifier. + /// + /// + [DisplayName(DisplayNames.RequestId)] + [JsonPropertyName(DisplayNames.RequestId)] + public string? RequestId { get; set; } + + /// + /// Gets or sets the keyVaultProvider identifier. + /// + /// + [DisplayName(DisplayNames.ClientId)] + [JsonPropertyName(DisplayNames.ClientId)] + public string? ClientId { get; set; } + + /// + /// Gets or sets the keyVaultProvider identifier. + /// + /// keyVaultProviderRequest + [DisplayName(DisplayNames.Operation)] + [JsonPropertyName(DisplayNames.Operation)] + public LogOperation Operation { get; set; } + + /// + /// Gets or sets the user name. + /// + /// keyVaultProviderRequest + [DisplayName(DisplayNames.User)] + [JsonPropertyName(DisplayNames.User)] + public string? User { get; set; } + + /// + /// Gets or sets user's email. + /// + /// keyVaultProviderRequest + [DisplayName(DisplayNames.Email)] + [JsonPropertyName(DisplayNames.Email)] + public string? Email { get; set; } + + /// + /// Gets or sets the user identifier. + /// + /// keyVaultProviderRequest + [DisplayName(DisplayNames.UserId)] + [JsonPropertyName(DisplayNames.UserId)] + public string? UserId { get; set; } + + /// + /// Gets or sets the message. + /// + /// A custom log message. + [DisplayName(DisplayNames.Message)] + [JsonPropertyName(DisplayNames.Message)] + public TMessage? Message { get; set; } + } +} diff --git a/Core.Blueprint.Logging/Adapters/LogOperation.cs b/Core.Blueprint.Logging/Adapters/LogOperation.cs new file mode 100644 index 0000000..33259b7 --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/LogOperation.cs @@ -0,0 +1,55 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// Represents all possible values for log operation. + /// + [DataContract] + public enum LogOperation + { + /// + /// The keyVaultProvider request log operation type. + /// + [EnumMember(Value = DisplayNames.ClientRequest)] + [JsonPropertyName(DisplayNames.ClientRequest)] + ClientRequest = 0, + + /// + /// The keyVaultProvider response log operation type. + /// + [EnumMember(Value = DisplayNames.ClientResponse)] + ClientResponse = 1, + + /// + /// The external request log operation type. + /// + [EnumMember(Value = DisplayNames.ExternalRequest)] + ExternalRequest = 2, + + /// + /// The external response log operation type. + /// + [EnumMember(Value = DisplayNames.ExternalResponse)] + ExternalResponse = 3, + + /// + /// The error log operation type. + /// + [EnumMember(Value = DisplayNames.Error)] + Error = 4, + + /// + /// The info log operation type. + /// + [EnumMember(Value = DisplayNames.Info)] + Info = 5, + } +} diff --git a/Core.Blueprint.Logging/Adapters/LogSeverity.cs b/Core.Blueprint.Logging/Adapters/LogSeverity.cs new file mode 100644 index 0000000..fac3b13 --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/LogSeverity.cs @@ -0,0 +1,41 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Runtime.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// Represents all possible values for log severity. + /// + [DataContract] + public enum LogSeverity + { + /// + /// The information severity level. + /// + [EnumMember(Value = DisplayNames.Information)] + Info = 0, + + /// + /// The warning severity level. + /// + [EnumMember(Value = DisplayNames.Warning)] + Warn = 1, + + /// + /// The error severity level. + /// + [EnumMember(Value = DisplayNames.Error)] + Error = 2, + + /// + /// The fatal severity level. + /// + [EnumMember(Value = DisplayNames.Fatal)] + Fatal = 3, + } +} diff --git a/Core.Blueprint.Logging/Adapters/LogTarget.cs b/Core.Blueprint.Logging/Adapters/LogTarget.cs new file mode 100644 index 0000000..7149856 --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/LogTarget.cs @@ -0,0 +1,41 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// The service logger target object. + /// + public class LogTarget + { + /// + /// Gets or sets the log target method. + /// + /// GET + [DisplayName(DisplayNames.Method)] + [JsonPropertyName(DisplayNames.Method)] + public string? Method { get; set; } + + /// + /// Gets or sets the log target host. + /// + /// GET + [DisplayName(DisplayNames.Host)] + [JsonPropertyName(DisplayNames.Host)] + public string? Host { get; set; } + + /// + /// Gets or sets the log target route. + /// + /// GET + [DisplayName(DisplayNames.Route)] + [JsonPropertyName(DisplayNames.Route)] + public string? Route { get; set; } + } +} diff --git a/Core.Blueprint.Logging/Adapters/ServiceSettings.cs b/Core.Blueprint.Logging/Adapters/ServiceSettings.cs new file mode 100644 index 0000000..b7f3c21 --- /dev/null +++ b/Core.Blueprint.Logging/Adapters/ServiceSettings.cs @@ -0,0 +1,20 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Blueprint.Logging +{ + /// + /// The service settings. + /// + public class ServiceSettings + { + /// + /// Gets or sets the service identifier. + /// + public string? ApplicationName { get; set; } + public string? LayerName { get; set; } + } +} diff --git a/Core.Blueprint.Logging/Configuration/Registerblueprint.cs b/Core.Blueprint.Logging/Configuration/Registerblueprint.cs new file mode 100644 index 0000000..418cd79 --- /dev/null +++ b/Core.Blueprint.Logging/Configuration/Registerblueprint.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Events; + +namespace Core.Blueprint.Logging.Configuration +{ + /// + /// Provides extension methods for configuring logging in the application. + /// + public static class Registerblueprint + { + /// + /// Registers logging services in the application. + /// + /// The to add the services to. + /// The for accessing configuration and application setup. + /// The updated . + public static IServiceCollection AddLogs(this IServiceCollection services, WebApplicationBuilder builder) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .CreateLogger(); + + builder.Host.UseSerilog(Log.Logger); + + services.AddScoped(); + + return services; + } + + /// + /// Configures middleware for logging and error handling in the application. + /// + /// The used to configure the middleware pipeline. + /// The service settings required by the middleware. + public static void UseLogging(this IApplicationBuilder app, IConfiguration configuration) + { + var serviceSettings = new ServiceSettings(); + configuration.GetSection(nameof(ServiceSettings)).Bind(serviceSettings); + + app.UseCustomHttpLogging(serviceSettings); + app.UseHttpExceptionHandler(serviceSettings); + } + + /// + /// Adds middleware to handle HTTP exceptions globally. + /// + /// The to add the middleware to. + /// The settings used by the exception handler middleware. + /// The updated . + public static IApplicationBuilder UseHttpExceptionHandler(this IApplicationBuilder builder, ServiceSettings settings) + { + return builder.UseMiddleware(settings); + } + + /// + /// Adds custom HTTP logging middleware to the application pipeline. + /// + /// The to add the middleware to. + /// The settings used by the logging middleware. + /// The updated . + public static IApplicationBuilder UseCustomHttpLogging(this IApplicationBuilder builder, ServiceSettings settings) + { + return builder.UseMiddleware(settings); + } + } +} diff --git a/Core.Blueprint.Logging/Constants/Claims.cs b/Core.Blueprint.Logging/Constants/Claims.cs new file mode 100644 index 0000000..26443b1 --- /dev/null +++ b/Core.Blueprint.Logging/Constants/Claims.cs @@ -0,0 +1,48 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +namespace Core.Blueprint.Logging +{ + /// + /// Constants for claims used in JWT tokens. + /// + public class Claims + { + /// + /// Claim name for user's name. + /// + public const string Name = "name"; + + /// + /// Claim name for user's name. + /// + public const string Email = "email"; + + /// + /// Claim name for user's ID. + /// + public const string Id = "id"; + + /// + /// Claim name for user's role ID. + /// + public const string Rol = "rol"; + + /// + /// Claim name for user's companies. + /// + public const string Companies = "companies"; + + /// + /// Claim name for user's projects. + /// + public const string Projects = "projects"; + + /// + /// Claim name for user's surveys. + /// + public const string Surveys = "surveys"; + } +} diff --git a/Core.Blueprint.Logging/Constants/DisplayNames.cs b/Core.Blueprint.Logging/Constants/DisplayNames.cs new file mode 100644 index 0000000..3fd50f3 --- /dev/null +++ b/Core.Blueprint.Logging/Constants/DisplayNames.cs @@ -0,0 +1,397 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Blueprint.Logging +{ + /// + /// Constants of the display names for this service. + /// + public static class DisplayNames + { + /// + /// The active patameter. + /// + public const string Active = "active"; + + + /// + /// The keyVaultProvider identifier parameter. + /// + public const string ClientId = "keyVaultProviderId"; + + /// + /// The keyVaultProvider request parameter. + /// + public const string ClientRequest = "keyVaultProviderRequest"; + + /// + /// The keyVaultProvider response parameter. + /// + public const string ClientResponse = "keyVaultProviderResponse"; + + /// + /// The creation date. + /// + public const string CreationDate = "creationDate"; + + /// + /// The content parameter. + /// + public const string Content = "content"; + + /// + /// The delete parameter. + /// + public const string Delete = "delete"; + + /// + /// The description parameter. + /// + public const string Description = "description"; + + /// + /// The detail parameter. + /// + public const string Detail = "detail"; + + + /// + /// The environment parameter. + /// + public const string Environment = "environment"; + + /// + /// The error log severity level parameter. + /// + public const string Error = "error"; + + /// + /// The error code parameter. + /// + public const string ErrorCode = "errorCode"; + + /// + /// The external request parameter. + /// + public const string ExternalRequest = "externalRequest"; + + /// + /// The external response parameter. + /// + public const string ExternalResponse = "externalResponse"; + + /// + /// The fatal log severity level parameter. + /// + public const string Fatal = "fatal"; + + /// + /// The host parameter. + /// + public const string Host = "host"; + + /// + /// The identifier parameter. + /// + public const string Id = "id"; + + /// + /// The inactive parameter. + /// + public const string Inactive = "inactive"; + + /// + /// The info log severity level parameter. + /// + public const string Info = "info"; + + /// + /// The information log severity level parameter. + /// + public const string Information = "information"; + + /// + /// The media parameter. + /// + public const string Media = "media"; + + /// + /// The media type parameter. + /// + public const string MediaType = "mediaType"; + + /// + /// The media use type parameter. + /// + public const string MediaUseType = "mediaUseType"; + + /// + /// Th message parameter. + /// + public const string Message = "message"; + + /// + /// The method parameter. + /// + public const string Method = "method"; + + /// + /// The monday parameter. + /// + public const string Monday = "monday"; + + /// + /// The MXN parameter. + /// + public const string MXN = "MXN"; + + /// + /// The name parameter. + /// + public const string Name = "name"; + + /// + /// The next page parameter. + /// + public const string NextPage = "nextPage"; + + /// + /// The nick name parameter. + /// + public const string NickName = "nickName"; + + /// + /// The note parameter. + /// + public const string Note = "note"; + + /// + /// The not so affordable parameter. + /// + public const string NotSoAffordable = "notSoAffordable"; + + /// + /// The object status parameter. + /// + public const string ObjectStatus = "objectStatus"; + + /// + /// The opening time parameter. + /// + public const string OpeningTime = "openingTime"; + + /// + /// The operation days parameter. + /// + public const string OperationDays = "operationDays"; + + /// + /// The page parameter. + /// + public const string Page = "page"; + + /// + /// The page count parameter. + /// + public const string PageCount = "pageCount"; + + /// + /// The page metadata parameter. + /// + public const string PageMetadata = "pageMetadata"; + + /// + /// The page size parameter. + /// + public const string PageSize = "pageSize"; + + /// + /// The parent identifier parameter. + /// + public const string ParentId = "ParentId"; + + /// + /// The pet ticket price parameter. + /// + public const string PetTicketPrice = "petTicketPrice"; + + /// + /// The place parameter. + /// + public const string Place = "place"; + + /// + /// The place type parameter. + /// + public const string PlaceType = "placeType"; + + /// + /// The previous page parameter. + /// + public const string PreviousPage = "previousPage"; + + /// + /// The provider identifier parameter. + /// + public const string ProviderId = "providerId"; + + /// + /// The provider type identifier parameter. + /// + public const string ProviderTypeId = "providerTypeId"; + + /// + /// The request identifier parameter. + /// + public const string RequestId = "requestId"; + + /// + /// The RNT identifier parameter. + /// + public const string RntId = "rntId"; + + /// + /// The route parameter. + /// + public const string Route = "route"; + + /// + /// The operation parameter. + /// + public const string Operation = "operation"; + + /// + /// The other ticket price parameter. + /// + public const string OtherTicketPrice = "otherTicketPrice"; + + /// + /// The path parameter. + /// + public const string Path = "path"; + + /// + /// The saturday parameter. + /// + public const string Saturday = "saturday"; + + /// + /// The secondary parameter. + /// + public const string Secondary = "secondary"; + + /// + /// The service health parameter. + /// + public const string ServiceHealth = "serviceHealth"; + + /// + /// The service identifier parameter. + /// + public const string ServiceId = "serviceId"; + + /// + /// The severity parameter. + /// + public const string Severity = "severity"; + + /// + /// The state identifier parameter. + /// + public const string StateId = "stateId"; + + /// + /// The region identifier parameter. + /// + public const string StateProvinceRegionId = "regionId"; + + /// + /// The sunday parameter. + /// + public const string Sunday = "sunday"; + + /// + /// The tax identifier parameter. + /// + public const string TaxId = "taxId"; + + /// + /// The target parameter. + /// + public const string Target = "target"; + + /// + /// The thursday parameter. + /// + public const string Thursday = "thursday"; + + /// + /// The timestamp parameter. + /// + public const string Timestamp = "timestamp"; + + /// + /// The total items parameter. + /// + public const string TotalItems = "totalItems"; + + /// + /// Gets or sets the transaction identifier parameter. + /// + public const string TransactionId = "transactionId"; + + /// + /// The tuesday parameter. + /// + public const string Tuesday = "tuesday"; + + /// + /// The URI parameter. + /// + public const string Uri = "uri"; + + /// + /// The update parameter. + /// + public const string Update = "update"; + + /// + /// The x-forwarded-for header parameter. + /// + public const string XForwardedFor = "xForwardedFor"; + + /// + /// The x-forwarded-for header parameter. + /// + public const string XForwardedForHeader = "X-Forwarded-For"; + + /// + /// The final currency identifier parameter. + /// + public const string CurrencyId = "currencyId"; + + /// + /// The user identifier parameter. + /// + public const string UserId = "userId"; + + /// + /// The user parameter. + /// + public const string User = "user"; + + /// + /// The warning log severity level. + /// + public const string Warning = "warning"; + + /// + /// The email parameter. + /// + public const string Email = "email"; + + } +} diff --git a/Core.Blueprint.Logging/Constants/EnvironmentVariables.cs b/Core.Blueprint.Logging/Constants/EnvironmentVariables.cs new file mode 100644 index 0000000..936e14a --- /dev/null +++ b/Core.Blueprint.Logging/Constants/EnvironmentVariables.cs @@ -0,0 +1,21 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Blueprint.Logging +{ + /// + /// Constants of the environment variables for this service. + /// + public static class EnvironmentVariables + { + /// + /// The stage environment vriable. + /// + public const string Stage = "ASPNETCORE_ENVIRONMENT"; + } +} + + diff --git a/Core.Blueprint.Logging/Constants/ErrorCodes.cs b/Core.Blueprint.Logging/Constants/ErrorCodes.cs new file mode 100644 index 0000000..2811fbc --- /dev/null +++ b/Core.Blueprint.Logging/Constants/ErrorCodes.cs @@ -0,0 +1,79 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Blueprint.Logging +{ + /// + /// Constants for the error codes. + /// + public static class ErrorCodes + { + /// + /// The generic entities not found error code. + /// + public const string EntitiesNotFound = "{0}EntitiesNotFound"; + + /// + /// The entity already exsits error message. + /// + public const string EntityAlreadyExists = "{0}EntityAlreadyExists"; + + /// + /// The generic entity not found error code. + /// + public const string EntityNotFound = "{0}EntityNotFound"; + + /// + /// The generic not supported error code. + /// + public const string EntityNotSupported = "{0}NotSupported"; + + /// + /// The internal server error code. + /// + public const string InternalServerError = "InternalServerError"; + + /// + /// The invalid parameters in mapper error code. + /// + public const string InvalidParametersMapper = "InvalidParametersMapper"; + + /// + /// The page size invalid value error code. + /// + public const string PageSizeInvalidValue = "PageSizeInvalidValue"; + + /// + /// The page ot of range error code. + /// + public const string PageOutOfRange = "PageOutOfRange"; + + /// + /// The property does not match the regular expresion error code. + /// + public const string PropertyDoesNotMatchRegex = "{0}PropertyDoesNotMatchRegex"; + + /// + /// The property is required error code. + /// + public const string PropertyIsRequired = "{0}PropertyIsRequired"; + + /// + /// The property length invalid error code. + /// + public const string PropertyLengthInvalid = "{0}PropertyLengthInvalid"; + + /// + /// The property must be in range error code. + /// + public const string PropertyMustBeInRange = "{0}PropertyMustBeInRange"; + + /// + /// The route not found error code. + /// + public const string RouteNotFound = "RouteNotFound"; + } +} diff --git a/Core.Blueprint.Logging/Constants/Headers.cs b/Core.Blueprint.Logging/Constants/Headers.cs new file mode 100644 index 0000000..05c6a2f --- /dev/null +++ b/Core.Blueprint.Logging/Constants/Headers.cs @@ -0,0 +1,10 @@ +namespace Core.Blueprint.Logging +{ + public static class Headers + { + /// + /// The authorization header. + /// + public const string Authorization = "Authorization"; + } +} diff --git a/Core.Blueprint.Logging/Constants/MimeTypes.cs b/Core.Blueprint.Logging/Constants/MimeTypes.cs new file mode 100644 index 0000000..515211f --- /dev/null +++ b/Core.Blueprint.Logging/Constants/MimeTypes.cs @@ -0,0 +1,148 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Globalization; + +namespace Core.Blueprint.Logging +{ + /// + /// Constants for the mime types. + /// + public static class MimeTypes + { + /// + /// The service application/json mime type. + /// + public const string ApplicationJson = "application/json"; + + /// + /// The application/pdf mime type. + /// + public const string ApplicationPdf = "application/pdf"; + + /// + /// The end index. + /// + public const int EndIndex = 5; + + /// + /// The JPEG extension. + /// + public const string ExtensionGif = "gif"; + + /// + /// The JPEG extension. + /// + public const string ExtensionJpeg = "jpeg"; + + /// + /// The PNG extension. + /// + public const string ExtensionPng = "png"; + + /// + /// The SVG extension. + /// + public const string ExtensionSvg = "svg"; + + /// + /// The image/gif mime type. + /// + public const string ImageGif = "image/gif"; + + /// + /// The image/jpeg mime type. + /// + public const string ImageJpeg = "image/jpeg"; + + /// + /// The image/png mime type. + /// + public const string ImagePng = "image/png"; + + /// + /// The image/svg+xml mime type. + /// + public const string ImageSvg = "image/svg+xml"; + + /// + /// The identifier GIF. + /// + public const string IdentifierGif = "R0LGO"; + + /// + /// The identifier PNG. + /// + public const string IdentifierJpeg = "/9J/4"; + + /// + /// The identifier PDF. + /// + public const string IdentifierPdf = "JVBER"; + + /// + /// The identifier PNG. + /// + public const string IdentifierPng = "IVBOR"; + + /// + /// The identifier SVG. + /// + public const string IdentifierSvg = "PHN2Z"; + + /// + /// The parameter name. + /// + public const string ParameterName = "MimeType"; + + /// + /// The start index. + /// + public const int StartIndex = 0; + + /// + /// The mime type dictionary. + /// + public static readonly Dictionary Dictionary = new Dictionary + { + { IdentifierJpeg, ImageJpeg }, + { IdentifierPng, ImagePng }, + { IdentifierGif, ImageGif }, + { IdentifierSvg, ImageSvg }, + }; + + /// + /// The mime type dictionary. + /// + public static readonly Dictionary DictionaryExtension = new Dictionary + { + { IdentifierJpeg, ExtensionJpeg }, + { IdentifierPng, ExtensionPng }, + { IdentifierGif, ExtensionGif }, + { IdentifierSvg, ExtensionSvg }, + }; + + /// + /// Gets the mime type. + /// + /// The cpntent with mime type identifier, substring 0, 5 from content. + /// A representing the value. + public static string GetMimeType(this string content) + { + return Dictionary.FirstOrDefault(_ => _.Key == content[..EndIndex].ToUpper(CultureInfo.InvariantCulture)).Value; + } + + /// + /// Gets the extension. + /// + /// The mime type identifier, substring 0, 5 from content. + /// A representing the value. + public static string GetExtension(this string content) + { + return DictionaryExtension.FirstOrDefault(_ => _.Key == content[..EndIndex].ToUpper(CultureInfo.InvariantCulture)).Value; + } + } +} diff --git a/Core.Blueprint.Logging/Constants/Responses.cs b/Core.Blueprint.Logging/Constants/Responses.cs new file mode 100644 index 0000000..ab1c8c2 --- /dev/null +++ b/Core.Blueprint.Logging/Constants/Responses.cs @@ -0,0 +1,29 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Blueprint.Logging +{ + /// + /// Constants of the responses for this service. + /// + public static class Responses + { + /// + /// The health response. + /// + public const string HealthyService = "healthy"; + + /// + /// The route does not exist response. + /// + public const string RouteDoesNotExist = "The specified route '{0}' does not exist for method '{1}' in this service."; + + /// + /// The target response. + /// + public const string Target = "{0}|{1}://{2}{3}"; + } +} diff --git a/Core.Blueprint.Logging/Contracts/ILoggerProvider.cs b/Core.Blueprint.Logging/Contracts/ILoggerProvider.cs new file mode 100644 index 0000000..95b427e --- /dev/null +++ b/Core.Blueprint.Logging/Contracts/ILoggerProvider.cs @@ -0,0 +1,12 @@ +namespace Core.Blueprint.Logging +{ + public interface ILoggerProvider + { + public void LogInformation(string service, params object[] args); + public void LogOperationStarted(string service, params object[] args); + public void LogOperationFinished(string service, params object[] args); + public void LogWarning(string message, params object[] args); + public void LogError(string servicee, params object[] args); + public void LogCritical(Exception exception, string message, params object[] args); + } +} \ No newline at end of file diff --git a/Core.Blueprint.Logging/Core.Blueprint.Logging.csproj b/Core.Blueprint.Logging/Core.Blueprint.Logging.csproj new file mode 100644 index 0000000..5570fb2 --- /dev/null +++ b/Core.Blueprint.Logging/Core.Blueprint.Logging.csproj @@ -0,0 +1,21 @@ + + + + + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Core.Blueprint.Logging/Middleware/HttpErrorMiddleware.cs b/Core.Blueprint.Logging/Middleware/HttpErrorMiddleware.cs new file mode 100644 index 0000000..2894e93 --- /dev/null +++ b/Core.Blueprint.Logging/Middleware/HttpErrorMiddleware.cs @@ -0,0 +1,92 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Microsoft.AspNetCore.Http; +using Serilog; +using System.Text.Json; + +namespace Core.Blueprint.Logging +{ + /// + /// Handles HTTP logging. + /// + public class HttpErrorMiddleware + { + private readonly ILogger logger; + private readonly RequestDelegate requestProcess; + public readonly ServiceSettings settings; + + /// + /// Creates a new instrance of . + /// + /// The logger representig an instance of . + /// The request delegate process. + public HttpErrorMiddleware(ILogger logger, RequestDelegate requestProcess, ServiceSettings settings) + { + this.logger = logger; + this.requestProcess = requestProcess; + this.settings = settings; + } + + /// + /// Invoke method. + /// + /// The HTTP context. + /// A representing the asynchronous operation. + public async Task Invoke(HttpContext context) + { + try + { + await requestProcess(context).ConfigureAwait(false); + } + catch (HttpException exception) + { + await HandleErrorResponse( + context, + exception.Message, + exception.ErrorCode, + exception.StatusCode).ConfigureAwait(false); + } + catch (Exception defaultException) + { + await HandleErrorResponse( + context, + defaultException.Message, + ErrorCodes.InternalServerError, + StatusCodes.Status500InternalServerError).ConfigureAwait(false); + } + } + + /// + /// Handles error responses. + /// + /// The HTTP context. + /// The error message. + /// The error code. + /// The HTTP status code. + /// A representing the asynchronous operation. + private async Task HandleErrorResponse(HttpContext context, string? message, string? errorCode, int statusCode) + { + var errorMessage = new HttpError( + message, + errorCode, + string.Format( + Responses.Target, + context.Request.Method, + context.Request.Scheme, + context.Request.Host.Host, + context.Request.Path)); + + logger.LogError(context, errorMessage, $"{settings.ApplicationName}-{settings.LayerName}"); + + context.Response.ContentType = MimeTypes.ApplicationJson; + context.Response.StatusCode = statusCode; + + await context.Response.WriteAsync(JsonSerializer.Serialize(errorMessage)).ConfigureAwait(false); + } + } +} + diff --git a/Core.Blueprint.Logging/Middleware/HttpLogger.cs b/Core.Blueprint.Logging/Middleware/HttpLogger.cs new file mode 100644 index 0000000..e994839 --- /dev/null +++ b/Core.Blueprint.Logging/Middleware/HttpLogger.cs @@ -0,0 +1,237 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Microsoft.AspNetCore.Http; +using Microsoft.IO; +using Serilog; +using System.IdentityModel.Tokens.Jwt; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Logging +{ + /// + /// Handles all logging scenarios. + /// + public static class HttpLogger + { + /// + /// The JSON serializer options for logging methods. + /// + public static JsonSerializerOptions serializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { + new JsonStringEnumConverter( JsonNamingPolicy.CamelCase), + }, + }; + + /// + /// Logs an error message. + /// + /// The generic message parameter. + /// The HTTP context. + /// The message. + /// The service identifier. + public static void LogError(this ILogger logger, HttpContext context, TMessage message, string? serviceId) + { + var logMessage = CreateErrorLog(context, message, serviceId); + logger.Error(logMessage); + } + + /// + /// Logs an information message. + /// + /// The generic message parameter. + /// The HTTP context. + /// The message. + /// The service identifier. + public static void LogInfo(this ILogger logger, HttpContext context, TMessage message, string? serviceId) + { + var logMessage = CreateInfoLog(context, message, serviceId); + logger.Information(logMessage); + } + + /// + /// Logs an incoming HTTP request. + /// + /// The logger. + /// The HTTP context. + /// The recyclable mmory stream manager. + /// The service identifier. + /// A representing the asynchronous operation. + public static async Task LogRequest( + this ILogger logger, + HttpContext context, + RecyclableMemoryStreamManager recyclableMemoryStreamManager, + string? serviceId) + { + context.Request.EnableBuffering(); + await using var requestStream = recyclableMemoryStreamManager.GetStream(); + await context.Request.Body.CopyToAsync(requestStream); + + var logMessage = CreateRequestLog( + context, + ReadStream(requestStream), + serviceId); + logger.Information(logMessage); + + context.Request.Body.Position = 0; + } + + /// + /// Logs an outcome HTTP response. + /// + /// The logger. + /// The HTTP context. + /// The recyclable mmory stream manager. + /// The request delegate process. + /// The service identifier. + /// A representing the asynchronous operation. + /// + public static async Task LogResponse( + this ILogger logger, + HttpContext context, + RecyclableMemoryStreamManager recyclableMemoryStreamManager, + RequestDelegate requestProcess, + string? serviceId) + { + var originalBodyStream = context.Response.Body; + await using var responseBody = recyclableMemoryStreamManager.GetStream(); + context.Response.Body = responseBody; + + await requestProcess(context); + + context.Response.Body.Seek(0, SeekOrigin.Begin); + var text = await new StreamReader(context.Response.Body).ReadToEndAsync(); + context.Response.Body.Seek(0, SeekOrigin.Begin); + + var logMessage = CreateResponseLog(context, text, serviceId); + logger.Information(logMessage); + + await responseBody.CopyToAsync(originalBodyStream); + } + + /// + /// Creates an error log. + /// + /// The generic message. + /// The HTTP context. + /// The error message. + /// The service identifier. + /// A representig the error log. + private static string CreateErrorLog(HttpContext context, TMessage message, string? serviceId) + => CreateLog(context, LogSeverity.Error, LogOperation.Error, message, serviceId); + + /// + /// Creates an info log. + /// + /// The generic message. + /// The HTTP context. + /// The info message. + /// The service identifier. + /// A representig the info log. + private static string CreateInfoLog(HttpContext context, TMessage message, string? serviceId) + => CreateLog(context, LogSeverity.Info, LogOperation.Info, message, serviceId); + + /// + /// Creates a request log. + /// + /// The HTTP context. + /// The request body. + /// The service identifier. + /// A representig the request log. + private static string CreateRequestLog(HttpContext context, string? requestBody, string? serviceId) + => CreateLog(context, LogSeverity.Info, LogOperation.ClientRequest, requestBody, serviceId); + + /// + /// Creates a response log. + /// + /// The HTTP context. + /// The response body. + /// The service identifier. + /// A representig the response log. + private static string CreateResponseLog(HttpContext context, string? responseBody, string? serviceId) + => CreateLog(context, LogSeverity.Info, LogOperation.ClientResponse, responseBody, serviceId); + + /// + /// Creates a generic log. + /// + /// The HTTP context. + /// The log severity. + /// The log operation. + /// The log message + /// The service identifier. + /// A representing a generic log. + private static string CreateLog( + HttpContext context, + LogSeverity severity, + LogOperation operation, + TMessage message, + string? serviceId) + { + var tokenHeader = context.Request.Headers[Headers.Authorization].FirstOrDefault()?.Split(" ").Last(); + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHeader is not null ? tokenHandler.ReadJwtToken(tokenHeader) : null; + + var log = new LogDetail + { + Severity = severity, + Target = new LogTarget + { + Method = context.Request.Method, + Host = context.Request.Host.Host, + Route = context.Request.Path, + }, + Email = token?.Claims.FirstOrDefault(c => c.Type == Claims.Email)?.Value, + User = token?.Claims.FirstOrDefault(c => c.Type == Claims.Name)?.Value, + UserId = token?.Claims.FirstOrDefault(c => c.Type == Claims.Id)?.Value, + Environment = Environment.GetEnvironmentVariable(EnvironmentVariables.Stage), + Operation = operation, + RequestId = context.Request.Headers[DisplayNames.RequestId], + ServiceId = serviceId, + XForwardedFor = context.Request.Headers[DisplayNames.XForwardedForHeader], + Timestamp = DateTime.Now, + Message = message, + }; + + var serializedLog = JsonSerializer.Serialize(log, serializerOptions); + + return serializedLog + .Replace("\\u0022", "\"") + .Replace("\"{", "{") + .Replace("}\"", "}") + .Replace("\\u0027", "'") + .Replace("\\\u0027", "'") + .Replace("\n", ""); + } + + /// + /// Reads the stream. + /// + /// The stream to be read. + /// A representig the request body. + private static string? ReadStream(Stream stream) + { + const int readChunkBufferLength = 4096; + stream.Seek(0, SeekOrigin.Begin); + using var textWriter = new StringWriter(); + using var reader = new StreamReader(stream); + var readChunk = new char[readChunkBufferLength]; + int readChunkLength; + do + { + readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength); + textWriter.Write(readChunk, 0, readChunkLength); + } while (readChunkLength > 0); + + var stringItem = textWriter.ToString(); + + return stringItem != string.Empty ? stringItem : null; + } + } +} + diff --git a/Core.Blueprint.Logging/Middleware/HttpLoggingMiddleware.cs b/Core.Blueprint.Logging/Middleware/HttpLoggingMiddleware.cs new file mode 100644 index 0000000..b0e4c9f --- /dev/null +++ b/Core.Blueprint.Logging/Middleware/HttpLoggingMiddleware.cs @@ -0,0 +1,69 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Microsoft.AspNetCore.Http; +using Microsoft.IO; +using Serilog; + +namespace Core.Blueprint.Logging +{ + /// + /// Handles HTTP logging. + /// + public class HttpLoggingMiddleware + { + private readonly ILogger logger; + private readonly RequestDelegate requestProcess; + private readonly ServiceSettings settings; + private readonly RecyclableMemoryStreamManager recyclableMemoryStreamManager; + + /// + /// Creates a new instrance of . + /// + /// The request delegate process. + /// The logger representig an instance of . + /// The service settings. + public HttpLoggingMiddleware(RequestDelegate requestProcess, ILogger logger, ServiceSettings settings) + { + this.logger = logger; + this.requestProcess = requestProcess; + this.settings = settings; + recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); + } + + /// + /// Invoke method. + /// + /// The HTTP context. + /// + public async Task Invoke(HttpContext context) + { + await LogRequest(context); + await LogResponse(context); + } + + /// + /// Logs an incoming HTTP request. + /// + /// The HTTP context. + /// A representing the asynchronous operation. + private async Task LogRequest(HttpContext context) + { + await logger.LogRequest(context, recyclableMemoryStreamManager, $"{settings.ApplicationName}-{settings.LayerName}"); + } + + /// + /// Logs an outcome HTTP response. + /// + /// The HTTP context. + /// A representing the asynchronous operation. + private async Task LogResponse(HttpContext context) + { + await logger.LogResponse(context, recyclableMemoryStreamManager, requestProcess, $"{settings.ApplicationName}-{settings.LayerName}"); + } + } +} + diff --git a/Core.Blueprint.Logging/Provider/LoggerProvider.cs b/Core.Blueprint.Logging/Provider/LoggerProvider.cs new file mode 100644 index 0000000..7716839 --- /dev/null +++ b/Core.Blueprint.Logging/Provider/LoggerProvider.cs @@ -0,0 +1,89 @@ +namespace Core.Blueprint.Logging +{ + /// + /// Provides logging functionalities using Serilog. + /// + public class LoggerProvider : ILoggerProvider + { + private readonly Serilog.ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Serilog logger instance. + public LoggerProvider(Serilog.ILogger logger) + { + this.logger = logger; + } + + /// + /// Logs an informational message for a specific service. + /// + /// The name of the service. + /// Additional arguments to include in the log. + public void LogInformation(string service, params object[] args) + { + logger.Information("Starting operation in {service} service", service, args); + } + + /// + /// Logs a message indicating the start of an operation in a specific service. + /// + /// The name of the service. + /// Additional parameters associated with the operation. + public void LogOperationStarted(string service, params object[] args) + { + logger.Information("Starting operation in {Service} service with parameters: {@Args}", service, args); + } + + /// + /// Logs a message indicating the completion of an operation in a specific service. + /// + /// The name of the service. + /// Additional parameters associated with the operation. + public void LogOperationFinished(string service, params object[] args) + { + logger.Information("Finishing operation in {Service} service with parameters: {@Args}", service, args); + } + + /// + /// Logs a general informational message. + /// + /// The message to log. + public void LogInformation(string message) + { + logger.Information(message); + } + + /// + /// Logs a warning message with additional context. + /// + /// The warning message to log. + /// Additional arguments to include in the log. + public void LogWarning(string message, params object[] args) + { + logger.Warning(message, args); + } + + /// + /// Logs an error that occurred in a specific service. + /// + /// The name of the service. + /// Additional details about the error. + public void LogError(string service, params object[] args) + { + logger.Error("An error occurred in `{service}` Exception: {@Args}", service, args); + } + + /// + /// Logs a critical error with an exception, message, and additional context. + /// + /// The exception associated with the critical error. + /// The critical error message. + /// Additional arguments to include in the log. + public void LogCritical(Exception exception, string message, params object[] args) + { + logger.Fatal(exception, message, args); + } + } +} diff --git a/Core.Blueprint.Mongo/Attributes/CollectionAttributeName.cs b/Core.Blueprint.Mongo/Attributes/CollectionAttributeName.cs new file mode 100644 index 0000000..a70d174 --- /dev/null +++ b/Core.Blueprint.Mongo/Attributes/CollectionAttributeName.cs @@ -0,0 +1,24 @@ +namespace Core.Blueprint.Mongo +{ + /// + /// The attribute is used to specify the name of a MongoDB collection + /// that a class should be mapped to. This attribute can be applied to classes that represent MongoDB entities. + /// + [AttributeUsage(AttributeTargets.Class)] // This attribute can only be applied to classes. + public class CollectionAttributeName : Attribute + { + /// + /// Gets or sets the name of the MongoDB collection that the class is mapped to. + /// + public string Name { get; set; } + + /// + /// Initializes a new instance of the class with the specified collection name. + /// + /// The name of the MongoDB collection that the class should be mapped to. + public CollectionAttributeName(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name), "Collection name cannot be null."); + } + } +} diff --git a/Core.Blueprint.Mongo/Configuration/IdentityProvider/HeathIdentityProvider.cs b/Core.Blueprint.Mongo/Configuration/IdentityProvider/HeathIdentityProvider.cs new file mode 100644 index 0000000..1b72949 --- /dev/null +++ b/Core.Blueprint.Mongo/Configuration/IdentityProvider/HeathIdentityProvider.cs @@ -0,0 +1,121 @@ +using Azure.Core; +using Azure.Identity; +using MongoDB.Driver.Authentication.Oidc; + +namespace Core.Blueprint.Mongo.Configuration +{ + /// + /// The class is responsible for acquiring an OpenID Connect (OIDC) + /// access token for MongoDB authentication using Azure Identity and Managed Identity credentials. + /// + public class HeathIdentityProvider : IOidcCallback + { + /// + /// The audience (resource identifier) for which the OIDC token is being requested. + /// + private readonly string _audience; + + /// + /// The environment in which the application is running (e.g., Development, Production). + /// + private readonly string _environment; + + /// + /// Initializes a new instance of the class with the specified audience. + /// + /// The audience (resource identifier) for which the OIDC token is being requested. + public HeathIdentityProvider(string audience) + { + _audience = audience; + _environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; + } + + /// + /// Synchronously retrieves the OIDC access token to authenticate to MongoDB. + /// + /// The callback parameters provided for the OIDC request. + /// A token to cancel the operation. + /// An OIDC access token to authenticate to MongoDB. + /// Thrown if an error occurs during the token acquisition process. + public OidcAccessToken GetOidcAccessToken(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + try + { + AccessToken token; + + TokenRequestContext tokenRequestContext = + new TokenRequestContext( + new[] { _audience } + ); + + if (_environment == "Local") + { + token = + new ChainedTokenCredential( + new ManagedIdentityCredential(), + new VisualStudioCredential(), + new VisualStudioCodeCredential(), + new SharedTokenCacheCredential() + ) + .GetToken(tokenRequestContext); + } + else + { + token = + new ManagedIdentityCredential() + .GetToken(tokenRequestContext); + } + + return new OidcAccessToken(token.Token, expiresIn: null); + } + catch (Exception ex) + { + throw new Exception($"An error occurred while trying to get the OIDC token to connect to the database, ERROR: {ex.Message}"); + } + } + + /// + /// Asynchronously retrieves the OIDC access token to authenticate to MongoDB. + /// + /// The callback parameters provided for the OIDC request. + /// A token to cancel the operation. + /// A task that represents the asynchronous operation, with an OIDC access token as the result. + /// Thrown if an error occurs during the token acquisition process. + public async Task GetOidcAccessTokenAsync(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + try + { + TokenRequestContext tokenRequestContext = + new TokenRequestContext( + new[] { _audience } + ); + + AccessToken token; + + if (_environment == "Local") + { + token = await new ChainedTokenCredential( + new ManagedIdentityCredential(), + new VisualStudioCredential(), + new VisualStudioCodeCredential(), + new SharedTokenCacheCredential() + ) + .GetTokenAsync(tokenRequestContext, cancellationToken) + .ConfigureAwait(false); + } + else + { + token = await new ManagedIdentityCredential() + .GetTokenAsync(tokenRequestContext, cancellationToken) + .ConfigureAwait(false); + } + + return new OidcAccessToken(token.Token, expiresIn: null); + } + catch (Exception ex) + { + throw new Exception($"An error occurred while trying to get the OIDC token to connect to the database, ERROR: {ex.Message}"); + } + } + } +} diff --git a/Core.Blueprint.Mongo/Configuration/RegisterBlueprint.cs b/Core.Blueprint.Mongo/Configuration/RegisterBlueprint.cs new file mode 100644 index 0000000..bd78c3b --- /dev/null +++ b/Core.Blueprint.Mongo/Configuration/RegisterBlueprint.cs @@ -0,0 +1,65 @@ +using Core.Blueprint.Mongo; +using Core.Blueprint.Mongo.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace Core.Blueprint.DAL.Mongo.Configuration +{ + /// + /// The class contains extension methods for registering the MongoDB context and configuration settings + /// to the in the dependency injection container. + /// + public static class RegisterBlueprint + { + /// + /// Adds the MongoDB layer services to the . + /// Registers the MongoDB context and configuration settings for MongoDB connection, database name, and audience. + /// + /// The to which the services will be added. + /// The used to load MongoDB settings. + /// The updated with MongoDB services registered. + public static IServiceCollection AddMongoLayer(this IServiceCollection services, IConfiguration configuration) + { + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; + + services.AddSingleton(); + + 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 (string.IsNullOrEmpty(ConnectionString) || string.IsNullOrEmpty(Databasename) || string.IsNullOrEmpty(Audience)) + throw new InvalidOperationException("Mongo connection is not configured correctly."); + + services.Configure(options => + { + options.ConnectionString = ConnectionString; + options.Databasename = Databasename; + options.Audience = Audience; + }); + + services.AddSingleton(serviceProvider => + { + var settings = serviceProvider.GetRequiredService>().Value; + var mongoClientSettings = MongoClientSettings.FromConnectionString(settings.ConnectionString); + mongoClientSettings.Credential = MongoCredential.CreateOidcCredential(new HeathIdentityProvider(settings.Audience)); + return new MongoClient(mongoClientSettings); + }); + + services.AddSingleton(serviceProvider => + { + var settings = serviceProvider.GetRequiredService>().Value; + var client = serviceProvider.GetRequiredService(); + return client.GetDatabase(settings.Databasename); + }); + + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); + + return services; + } + } +} diff --git a/Core.Blueprint.Mongo/Context/MongoContext.cs b/Core.Blueprint.Mongo/Context/MongoContext.cs new file mode 100644 index 0000000..8561bbd --- /dev/null +++ b/Core.Blueprint.Mongo/Context/MongoContext.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Configuration; + +namespace Core.Blueprint.Mongo +{ + /// + /// The class represents the MongoDB context that contains the connection information, + /// including the connection string, database name, and audience. + /// It implements the interface to provide methods for accessing these values. + /// + public sealed class MongoContext : IMongoContext + { + /// + /// Gets or sets the connection string used to connect to the MongoDB instance. + /// + public string ConnectionString { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the MongoDB database. + /// + public string Databasename { get; set; } = string.Empty; + + /// + /// Gets or sets the audience (resource identifier) used for MongoDB authentication. + /// + public string Audience { get; set; } = string.Empty; + + private readonly IConfiguration configuration; + + /// + /// Initializes a new instance of the class using the provided . + /// The configuration is used to retrieve MongoDB connection settings. + /// + /// The configuration used to retrieve the MongoDB connection settings. + public MongoContext(IConfiguration configuration) + { + this.configuration = configuration; + } + + /// + /// Retrieves the MongoDB connection string from the configuration. + /// + /// The MongoDB connection string, or an empty string if not found. + public string GetConnectionString() + { + return configuration.GetConnectionString("MongoDb:ConnectionString")?.ToString() ?? string.Empty; + } + + /// + /// Retrieves the MongoDB database name from the configuration. + /// + /// The MongoDB database name, or an empty string if not found. + public string GetDatabasename() + { + return configuration.GetSection("MongoDb:DatabaseName").Value ?? string.Empty; + } + + /// + /// Retrieves the MongoDB audience (resource identifier) from the configuration. + /// + /// The MongoDB audience, or an empty string if not found. + public string GetAudience() + { + return configuration.GetSection("MongoDb:Audience").Value ?? string.Empty; + } + } +} diff --git a/Core.Blueprint.Mongo/Context/MongoDbSettings.cs b/Core.Blueprint.Mongo/Context/MongoDbSettings.cs new file mode 100644 index 0000000..2f803cb --- /dev/null +++ b/Core.Blueprint.Mongo/Context/MongoDbSettings.cs @@ -0,0 +1,25 @@ +namespace Core.Blueprint.Mongo +{ + /// + /// Represents the MongoDB configuration settings, including the connection string, + /// database name, and audience, used for connecting and authenticating to a MongoDB instance. + /// Implements the interface to provide a strongly typed configuration. + /// + public class MongoDbSettings : IMongoDbSettings + { + /// + /// Gets or sets the connection string used to connect to the MongoDB instance. + /// + public string ConnectionString { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the MongoDB database to connect to. + /// + public string Databasename { get; set; } = string.Empty; + + /// + /// Gets or sets the audience (resource identifier) used for MongoDB authentication. + /// + public string Audience { get; set; } = string.Empty; + } +} diff --git a/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs b/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs new file mode 100644 index 0000000..644452d --- /dev/null +++ b/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs @@ -0,0 +1,152 @@ +using MongoDB.Driver; +using System.Linq.Expressions; + +namespace Core.Blueprint.Mongo +{ + /// + /// Interface for performing CRUD operations and queries on MongoDB collections. + /// The represents the type of documents in the collection, + /// which must implement the interface. + /// + /// The type of document in the MongoDB collection, must implement . + public interface ICollectionsRepository where TDocument : IDocument + { + /// + /// Retrieves all documents from the collection as an enumerable queryable result. + /// + /// A containing an representing the collection's documents. + ValueTask> AsQueryable(); + + /// + /// Filters the documents in the collection by the provided filter expression. + /// + /// An expression used to filter the documents based on the provided condition. + /// A representing the asynchronous operation, with a result of an of filtered documents. + Task> FilterBy( + Expression> filterExpression); + + /// + /// Filters the documents in the collection by the provided filter expression and projects them to a different type. + /// + /// The type to project the documents into. + /// An expression used to filter the documents. + /// An expression used to project the filtered documents into the type. + /// An representing the projected documents. + IEnumerable FilterBy( + Expression> filterExpression, + Expression> projectionExpression); + + /// + /// Filters documents in the collection based on the provided MongoDB filter definition. + /// + /// A filter definition for MongoDB query. + /// A task that represents the asynchronous operation. The task result contains a list of documents that match the filter. + Task> FilterByMongoFilterAsync(FilterDefinition filterDefinition); + + /// + /// Finds a single document by the provided filter expression. + /// + /// An expression used to filter the documents. + /// The first matching or null if no match is found. + TDocument FindOne(Expression> filterExpression); + + /// + /// Asynchronously finds a single document by the provided filter expression. + /// + /// An expression used to filter the documents. + /// A representing the asynchronous operation, with the matching or null. + Task FindOneAsync(Expression> filterExpression); + + /// + /// Finds a document by its identifier. + /// + /// The identifier of the document. + /// The document with the provided identifier or null if not found. + TDocument FindById(string id); + + /// + /// Asynchronously finds a document by its identifier. + /// + /// The identifier of the document. + /// A representing the asynchronous operation, with the matching or null. + Task FindByIdAsync(string id); + + /// + /// Inserts a single document into the collection. + /// + /// The document to insert. + void InsertOne(TDocument document); + + /// + /// Asynchronously inserts a single document into the collection. + /// + /// The document to insert. + /// A representing the asynchronous operation. + Task InsertOneAsync(TDocument document); + + /// + /// Inserts multiple documents into the collection. + /// + /// The collection of documents to insert. + void InsertMany(ICollection documents); + + /// + /// Asynchronously inserts multiple documents into the collection. + /// + /// The collection of documents to insert. + /// A representing the asynchronous operation. + Task InsertManyAsync(ICollection documents); + + /// + /// Replaces an existing document with a new one. + /// + /// The document to replace the existing one. + void ReplaceOne(TDocument document); + + /// + /// Asynchronously replaces an existing document with a new one. + /// + /// The document to replace the existing one. + /// A representing the asynchronous operation. + Task ReplaceOneAsync(TDocument document); + + /// + /// Deletes a single document by the provided filter expression. + /// + /// An expression used to filter the documents to delete. + void DeleteOne(Expression> filterExpression); + + /// + /// Asynchronously deletes a single document by the provided filter expression. + /// + /// An expression used to filter the documents to delete. + /// A representing the asynchronous operation. + Task DeleteOneAsync(Expression> filterExpression); + + /// + /// Deletes a single document by its identifier. + /// + /// The identifier of the document to delete. + void DeleteById(string id); + + /// + /// Asynchronously deletes a single document by its identifier. + /// + /// The identifier of the document to delete. + /// A representing the asynchronous operation. + Task DeleteByIdAsync(string id); + + /// + /// Deletes multiple documents that match the provided filter expression. + /// + /// An expression used to filter the documents to delete. + void DeleteMany(Expression> filterExpression); + + /// + /// Asynchronously deletes multiple documents that match the provided filter expression. + /// + /// An expression used to filter the documents to delete. + /// A representing the asynchronous operation. + Task DeleteManyAsync(Expression> filterExpression); + } +} diff --git a/Core.Blueprint.Mongo/Contracts/IDocument.cs b/Core.Blueprint.Mongo/Contracts/IDocument.cs new file mode 100644 index 0000000..ae0d4d1 --- /dev/null +++ b/Core.Blueprint.Mongo/Contracts/IDocument.cs @@ -0,0 +1,39 @@ +using Core.Blueprint.Mongo; + +public interface IDocument +{ + /// + /// Gets or sets the MongoDB ObjectId for the document. + /// + string _Id { get; } + + /// + /// Gets or sets a unique identifier for the document, represented as a string (GUID). + /// + string Id { get; } + + /// + /// Gets or sets the timestamp of when the document was created. + /// + DateTime CreatedAt { get; } + + /// + /// Gets or sets the user or system who created the document. + /// + string? CreatedBy { get; set; } + + /// + /// Gets or sets the timestamp of when the document was last updated. + /// + DateTime? UpdatedAt { get; set; } + + /// + /// Gets or sets the user or system who last updated the document. + /// + string? UpdatedBy { get; set; } + + /// + /// Gets or sets the status of the document. + /// + StatusEnum? Status { get; set; } +} \ No newline at end of file diff --git a/Core.Blueprint.Mongo/Contracts/IMongoContext.cs b/Core.Blueprint.Mongo/Contracts/IMongoContext.cs new file mode 100644 index 0000000..410e446 --- /dev/null +++ b/Core.Blueprint.Mongo/Contracts/IMongoContext.cs @@ -0,0 +1,45 @@ +namespace Core.Blueprint.Mongo +{ + /// + /// Represents the context for interacting with MongoDB, providing access to connection-related information, + /// such as the connection string, database name, and audience for authentication. + /// + public interface IMongoContext + { + /// + /// Gets the connection string used to connect to the MongoDB instance. + /// + /// A string representing the MongoDB connection string. + string GetConnectionString(); + + /// + /// Gets the name of the MongoDB database. + /// + /// A string representing the MongoDB database name. + string GetDatabasename(); + + /// + /// Gets the audience (resource identifier) used for MongoDB authentication. + /// + /// A string representing the MongoDB audience (typically the resource identifier for authentication). + string GetAudience(); + + /// + /// Gets or sets the MongoDB connection string used to connect to the database. + /// + /// A string representing the MongoDB connection string. + string ConnectionString { get; set; } + + /// + /// Gets or sets the name of the MongoDB database. + /// + /// A string representing the MongoDB database name. + string Databasename { get; set; } + + /// + /// Gets or sets the audience (resource identifier) for MongoDB authentication. + /// + /// A string representing the MongoDB audience (resource identifier for authentication). + string Audience { get; set; } + } +} diff --git a/Core.Blueprint.Mongo/Contracts/IMongoDbSettings.cs b/Core.Blueprint.Mongo/Contracts/IMongoDbSettings.cs new file mode 100644 index 0000000..fff23cc --- /dev/null +++ b/Core.Blueprint.Mongo/Contracts/IMongoDbSettings.cs @@ -0,0 +1,32 @@ +namespace Core.Blueprint.Mongo +{ + /// + /// Represents the settings required to connect to a MongoDB instance, including the connection string, + /// database name, and audience used for authentication. + /// + public interface IMongoDbSettings + { + /// + /// Gets or sets the connection string used to connect to the MongoDB instance. + /// The connection string includes details such as server address, port, + /// authentication credentials, and any additional options needed for connection. + /// + /// A string representing the MongoDB connection string. + string ConnectionString { get; set; } + + /// + /// Gets or sets the name of the MongoDB database to connect to. + /// This value specifies which database to use within the MongoDB instance. + /// + /// A string representing the name of the MongoDB database. + string Databasename { get; set; } + + /// + /// Gets or sets the audience (resource identifier) for MongoDB authentication. + /// This is typically used in token-based authentication schemes (e.g., OAuth or OpenID Connect), + /// to specify the intended recipient of an access token. + /// + /// A string representing the MongoDB audience (resource identifier for authentication). + string Audience { get; set; } + } +} diff --git a/Core.Blueprint.Mongo/Core.Blueprint.Mongo.csproj b/Core.Blueprint.Mongo/Core.Blueprint.Mongo.csproj new file mode 100644 index 0000000..e75ebb1 --- /dev/null +++ b/Core.Blueprint.Mongo/Core.Blueprint.Mongo.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/Core.Blueprint.Mongo/Entities/Document.cs b/Core.Blueprint.Mongo/Entities/Document.cs new file mode 100644 index 0000000..3d164f0 --- /dev/null +++ b/Core.Blueprint.Mongo/Entities/Document.cs @@ -0,0 +1,81 @@ +using Core.Blueprint.Mongo; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +public class Document : IDocument +{ + /// + /// Gets or sets the MongoDB ObjectId for the document. + /// This property is automatically generated if not provided. + /// It is used as the primary key for the document in MongoDB. + /// + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + [JsonPropertyName("_id")] + public string _Id { get; init; } + + /// + /// Gets or sets a unique identifier for the document, represented as a string (GUID). + /// This value is automatically generated if not provided and can be used as a secondary key. + /// + [BsonElement("id")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("id")] + public string Id { get; init; } + + /// + /// Gets or sets the timestamp of when the document was created. + /// This value is automatically set to the current UTC time when the document is created. + /// + [BsonElement("createdAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; init; } + + /// + /// Gets or sets the user or system who created the document. + /// This field can be used for audit purposes. + /// + [BsonElement("createdBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("createdBy")] + public string? CreatedBy { get; set; } + + /// + /// Gets or sets the timestamp of when the document was last updated. + /// This value is nullable and will be set to null if the document has never been updated. + /// + [BsonElement("updatedAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } = null; + + /// + /// Gets or sets the user or system who last updated the document. + /// This field can be used for audit purposes. + /// + [BsonElement("updatedBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("updatedBy")] + public string? UpdatedBy { get; set; } = null; + + /// + /// Gets or sets the status of the document. + /// The status is represented by an enum and defaults to . + /// + [BsonElement("status")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public StatusEnum? Status { get; set; } + + public Document() + { + _Id = ObjectId.GenerateNewId().ToString(); + Id = Guid.NewGuid().ToString(); + CreatedAt = DateTime.UtcNow; + Status = StatusEnum.Active; + } +} \ No newline at end of file diff --git a/Core.Blueprint.Mongo/Entities/StatusEnum.cs b/Core.Blueprint.Mongo/Entities/StatusEnum.cs new file mode 100644 index 0000000..f66938c --- /dev/null +++ b/Core.Blueprint.Mongo/Entities/StatusEnum.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace Core.Blueprint.Mongo +{ + /// + /// Represents the status of a document or entity. This enum is used to track the state of a document + /// within the system. The `JsonStringEnumConverter` ensures that the enum values are serialized as strings + /// in JSON format, rather than as numeric values. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum StatusEnum + { + /// + /// Represents an active document or entity. + /// + Active = 0, + + /// + /// Represents an inactive document or entity. + /// + Inactive = 1, + + /// + /// Represents a deleted document or entity. + /// + Deleted = 2 + } +} \ No newline at end of file diff --git a/Core.Blueprint.Mongo/Provider/MongoProvider.cs b/Core.Blueprint.Mongo/Provider/MongoProvider.cs new file mode 100644 index 0000000..1f1f764 --- /dev/null +++ b/Core.Blueprint.Mongo/Provider/MongoProvider.cs @@ -0,0 +1,40 @@ +using MongoDB.Driver; + +namespace Core.Blueprint.Mongo +{ + /// + /// Provides the MongoDB provider and database connection using the specified configuration settings. + /// This class manages the connection to MongoDB and ensures that the correct credentials are used for authentication. + /// + public class MongoProvider + { + private readonly IMongoDatabase _database; + + /// + /// Initializes a new instance of the class. + /// This constructor sets up the MongoDB provider using the connection string, audience, and other settings provided. + /// It also configures authentication using OpenID Connect (OIDC) credentials. + /// + /// The MongoDB settings required for connecting to the database. + public MongoProvider(IMongoDatabase database) + { + _database = database ?? throw new ArgumentNullException(nameof(database)); + } + + /// + /// Gets the initialized MongoDB database. If the database is not initialized, an exception is thrown. + /// + /// Thrown when the database connection is not initialized. + protected IMongoDatabase Database + { + get + { + if (_database == null) + { + throw new InvalidOperationException("MongoDB connection is not initialized."); + } + return _database; + } + } + } +} \ No newline at end of file diff --git a/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs b/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs new file mode 100644 index 0000000..9214251 --- /dev/null +++ b/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs @@ -0,0 +1,252 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Core.Blueprint.Mongo +{ + /// + /// Provides methods for interacting with a MongoDB collection for a specific document type. + /// Inherits from and implements . + /// This class encapsulates common database operations such as querying, inserting, updating, and deleting documents. + /// + /// The type of document stored in the collection, which must implement . + public class CollectionRepository(IMongoDatabase database) : ICollectionsRepository where TDocument : IDocument + { + private IMongoCollection _collection; + + /// + /// Initializes the MongoDB collection based on the attribute + /// applied to the type. Throws an exception if the attribute is not present. + /// + public void CollectionInitialization() + { + var collectionAttribute = typeof(TDocument).GetCustomAttribute(); + if (collectionAttribute == null) + { + throw new InvalidOperationException($"The class {typeof(TDocument).Name} is missing the CollectionAttributeName attribute."); + } + + string collectionName = collectionAttribute.Name; + + _collection = database.GetCollection(collectionName); + } + + /// + /// Returns all documents from the collection as an enumerable list. + /// + /// A task that represents the asynchronous operation. The task result contains an enumerable list of documents. + public virtual async ValueTask> AsQueryable() + { + return await _collection.AsQueryable().ToListAsync(); + } + + /// + /// Filters documents in the collection based on the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the documents. + /// A task that represents the asynchronous operation. The task result contains a list of documents that match the filter. + public virtual async Task> FilterBy( + Expression> filterExpression) + { + var objectResult = await _collection.FindAsync(filterExpression).ConfigureAwait(false); + return objectResult.ToList(); + } + + /// + /// Filters documents in the collection based on the provided filter and projection expressions. + /// Projects the filtered documents into a new type. + /// + /// The type to project the documents into. + /// A lambda expression that defines the filter criteria for the documents. + /// A lambda expression that defines how the documents should be projected. + /// An enumerable collection of projected documents. + public virtual IEnumerable FilterBy( + Expression> filterExpression, + Expression> projectionExpression) + { + return _collection.Find(filterExpression).Project(projectionExpression).ToEnumerable(); + } + + /// + /// Finds a single document that matches the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the document. + /// The document that matches the filter, or null if no document is found. + public virtual TDocument FindOne(Expression> filterExpression) + { + return _collection.Find(filterExpression).FirstOrDefault(); + } + + /// + /// Filters documents in the collection based on the provided MongoDB filter definition. + /// + /// A filter definition for MongoDB query. + /// A task that represents the asynchronous operation. The task result contains a list of documents that match the filter. + public virtual async Task> FilterByMongoFilterAsync(FilterDefinition filterDefinition) + { + var objectResult = await _collection.Find(filterDefinition).ToListAsync().ConfigureAwait(false); + return objectResult; + } + + /// + /// Asynchronously finds a single document that matches the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the document. + /// A task that represents the asynchronous operation. The task result contains the document that matches the filter, or null if no document is found. + public virtual Task FindOneAsync(Expression> filterExpression) + { + return Task.Run(() => _collection.Find(filterExpression).FirstOrDefaultAsync()); + } + + /// + /// Finds a document by its unique identifier (ID). + /// + /// The unique identifier of the document. + /// The document that matches the specified ID, or null if no document is found. + public virtual TDocument FindById(string id) + { + var filter = Builders.Filter.Eq(doc => doc._Id, id); + return _collection.Find(filter).SingleOrDefault(); + } + + /// + /// Asynchronously finds a document by its unique identifier (ID). + /// + /// The unique identifier of the document. + /// A task that represents the asynchronous operation. The task result contains the document that matches the specified ID, or null if no document is found. + public virtual Task FindByIdAsync(string id) + { + return Task.Run(() => + { + var objectId = new ObjectId(id); + var filter = Builders.Filter.Eq(doc => doc._Id, id); + return _collection.Find(filter).SingleOrDefaultAsync(); + }); + } + + /// + /// Inserts a single document into the collection. + /// + /// The document to insert. + public virtual void InsertOne(TDocument document) + { + _collection.InsertOne(document); + } + + /// + /// Asynchronously inserts a single document into the collection. + /// + /// The document to insert. + /// A task that represents the asynchronous operation. + public virtual Task InsertOneAsync(TDocument document) + { + return Task.Run(() => _collection.InsertOneAsync(document)); + } + + /// + /// Inserts multiple documents into the collection. + /// + /// The collection of documents to insert. + public void InsertMany(ICollection documents) + { + _collection.InsertMany(documents); + } + + /// + /// Asynchronously inserts multiple documents into the collection. + /// + /// The collection of documents to insert. + /// A task that represents the asynchronous operation. + public virtual async Task InsertManyAsync(ICollection documents) + { + await _collection.InsertManyAsync(documents); + } + + /// + /// Replaces an existing document in the collection. + /// + /// The document with the updated data. + public void ReplaceOne(TDocument document) + { + var filter = Builders.Filter.Eq(doc => doc._Id, document._Id); + _collection.FindOneAndReplace(filter, document); + } + + /// + /// Asynchronously replaces an existing document in the collection. + /// + /// The document with the updated data. + /// A task that represents the asynchronous operation. + public virtual async Task ReplaceOneAsync(TDocument document) + { + var filter = Builders.Filter.Eq(doc => doc._Id, document._Id); + await _collection.FindOneAndReplaceAsync(filter, document); + } + + /// + /// Deletes a single document from the collection based on the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the document to delete. + public void DeleteOne(Expression> filterExpression) + { + _collection.FindOneAndDelete(filterExpression); + } + + /// + /// Asynchronously deletes a single document from the collection based on the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the document to delete. + /// A task that represents the asynchronous operation. + public async Task DeleteOneAsync(Expression> filterExpression) + { + return await _collection.FindOneAndDeleteAsync(filterExpression); + } + + /// + /// Deletes a single document from the collection based on its unique identifier (ID). + /// + /// The unique identifier of the document to delete. + public void DeleteById(string id) + { + var objectId = new ObjectId(id); + var filter = Builders.Filter.Eq(doc => doc._Id, id); + _collection.FindOneAndDelete(filter); + } + + /// + /// Asynchronously deletes a single document from the collection based on its unique identifier (ID). + /// + /// The unique identifier of the document to delete. + /// A task that represents the asynchronous operation. + public Task DeleteByIdAsync(string id) + { + return Task.Run(() => + { + var objectId = new ObjectId(id); + var filter = Builders.Filter.Eq(doc => doc._Id, id); + _collection.FindOneAndDeleteAsync(filter); + }); + } + + /// + /// Deletes multiple documents from the collection based on the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the documents to delete. + public void DeleteMany(Expression> filterExpression) + { + _collection.DeleteMany(filterExpression); + } + + /// + /// Asynchronously deletes multiple documents from the collection based on the provided filter expression. + /// + /// A lambda expression that defines the filter criteria for the documents to delete. + /// A task that represents the asynchronous operation. + public Task DeleteManyAsync(Expression> filterExpression) + { + return Task.Run(() => _collection.DeleteManyAsync(filterExpression)); + } + } +} diff --git a/Core.Blueprint.Redis/Adapters/CacheSettings.cs b/Core.Blueprint.Redis/Adapters/CacheSettings.cs new file mode 100644 index 0000000..0d357b4 --- /dev/null +++ b/Core.Blueprint.Redis/Adapters/CacheSettings.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Core.Blueprint.Redis +{ + public interface ICacheSettings + { + int DefaultCacheDurationInMinutes { get; set; } + } + /// + /// Represents the settings for Redis caching. + /// + public class CacheSettings: ICacheSettings + { + /// + /// Gets or sets the default cache duration in minutes. + /// + public int DefaultCacheDurationInMinutes { get; set; } + } +} diff --git a/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs b/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs new file mode 100644 index 0000000..3aeb596 --- /dev/null +++ b/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Core.Blueprint.Redis.Configuration +{ + /// + /// Provides extension methods for registering Redis-related services in the DI container. + /// + public static class RegisterBlueprint + { + /// + /// Adds Redis caching services to the service collection. + /// + /// The service collection to register the services into. + /// The application configuration object. + /// The updated service collection. + public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) + { + // Retrieve the Redis connection string from the configuration. + // Get Redis configuration section + var redisConnectionString = configuration.GetSection("ConnectionStrings:Redis").Value; + if (string.IsNullOrEmpty(redisConnectionString)) + { + throw new InvalidOperationException("Redis connection is not configured."); + } + + // Register RedisCacheProvider + services.AddSingleton(provider => + new RedisCacheProvider(redisConnectionString, provider.GetRequiredService>())); + + // Get CacheSettings and register with the ICacheSettings interface + var cacheSettings = configuration.GetSection("CacheSettings").Get(); + if (cacheSettings == null) + { + throw new InvalidOperationException("Redis CacheSettings section is not configured."); + } + services.AddSingleton(cacheSettings); + + return services; + } + } +} \ No newline at end of file diff --git a/Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs b/Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs new file mode 100644 index 0000000..f9a7b5e --- /dev/null +++ b/Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs @@ -0,0 +1,48 @@ +namespace Core.Blueprint.Redis +{ + /// + /// Interface for managing Redis cache operations. + /// + public interface IRedisCacheProvider + { + /// + /// Retrieves a cache item by its key. + /// + /// The type of the cached item. + /// The cache key. + /// The cached item, or default if not found. + ValueTask GetAsync(string key); + + /// + /// Sets a cache item with the specified key and value. + /// + /// The type of the item to cache. + /// The cache key. + /// The item to cache. + /// The optional expiration time for the cache item. + /// A task representing the asynchronous operation. + ValueTask SetAsync(string key, TEntity value, TimeSpan? expiry = null); + + /// + /// Removes a cache item by its key. + /// + /// The cache key. + /// A task representing the asynchronous operation. + ValueTask RemoveAsync(string key); + + /// + /// Checks if a cache item exists for the specified key. + /// + /// The cache key. + /// True if the cache item exists; otherwise, false. + ValueTask ExistsAsync(string key); + + /// + /// Refreshes the expiration time of a cache item if it exists. + /// + /// The cache key. + /// The new expiration time for the cache item. + /// A task representing the asynchronous operation. + ValueTask RefreshAsync(string key, TimeSpan? expiry = null); + } +} diff --git a/Core.Blueprint.Redis/Core.Blueprint.Redis.csproj b/Core.Blueprint.Redis/Core.Blueprint.Redis.csproj new file mode 100644 index 0000000..e1322ba --- /dev/null +++ b/Core.Blueprint.Redis/Core.Blueprint.Redis.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs b/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs new file mode 100644 index 0000000..f7f53cb --- /dev/null +++ b/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Core.Blueprint.Redis.Helpers +{ + /// + /// Helper class for generating consistent and normalized cache keys. + /// + public static class CacheKeyHelper + { + /// + /// Generates a cache key based on the instance, method name, and parameters. + /// + /// The instance of the class. + /// The method name related to the cache key. + /// The parameters used to generate the key. + /// A normalized cache key string. + public static string GenerateCacheKey(object instance, string methodName, params object[] parameters) + { + var className = instance.GetType().Name; + var keyBuilder = new StringBuilder($"{className}.{methodName}"); + + foreach (var param in parameters) + { + string normalizedParam = NormalizeParameter(param); + keyBuilder.Append($".{normalizedParam}"); + } + + return keyBuilder.ToString(); + } + + /// + /// Normalizes a parameter value for use in a cache key. + /// + /// The parameter to normalize. + /// A normalized string representation of the parameter. + private static string NormalizeParameter(object param) + { + if (param == null) + { + return "null"; + } + + string paramString; + + if (param is DateTime dateTime) + { + paramString = dateTime.ToString("yyyyMMdd"); + } + else + { + paramString = param.ToString(); + } + + // Replace special characters with an underscore. + return Regex.Replace(paramString, @"[^a-zA-Z0-9]", "_"); + } + } +} diff --git a/Core.Blueprint.Redis/RedisCacheProvider.cs b/Core.Blueprint.Redis/RedisCacheProvider.cs new file mode 100644 index 0000000..525e310 --- /dev/null +++ b/Core.Blueprint.Redis/RedisCacheProvider.cs @@ -0,0 +1,171 @@ +using Azure.Identity; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System.Text.Json; + +namespace Core.Blueprint.Redis +{ + /// + /// Redis cache provider for managing cache operations. + /// + public sealed class RedisCacheProvider : IRedisCacheProvider + { + private IDatabase _cacheDatabase = null!; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Redis connection string. + /// The logger instance for logging operations. + /// Thrown when connection string is null or empty. + public RedisCacheProvider(string connectionString, ILogger logger) + { + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentNullException(nameof(connectionString), "Redis connection string cannot be null or empty."); + + _logger = logger; + _cacheDatabase = InitializeRedisAsync(connectionString).GetAwaiter().GetResult(); + } + + /// + /// Initializes and establishes a connection to Redis using the provided connection string. + /// + /// The Redis connection string. + /// An instance representing the Redis cache database. + /// Thrown when the connection to Redis fails. InitializeRedisAsync(string connectionString) + { + try + { + var configurationOptions = await ConfigurationOptions.Parse($"{connectionString}") + .ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential()); + + configurationOptions.AbortOnConnectFail = false; + var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions); + + _logger.LogInformation("Successfully connected to Redis."); + + return connectionMultiplexer.GetDatabase(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error establishing Redis connection."); + throw; + } + } + + /// + /// Retrieves a cache item by its key. + /// + /// The type of the cached item. + /// The cache key. + /// The cached item of type , or default if not found. + public async ValueTask GetAsync(string key) + { + try + { + 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(value); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error getting cache item with key {key}"); + throw; + } + } + /// + /// Sets a cache item with the specified key and value. + /// + /// The type of the item to cache. + /// The cache key. + /// The item to cache. + /// The optional expiration time for the cache item. + public async ValueTask SetAsync(string key, TEntity value, TimeSpan? expiry = null) + { + try + { + var json = JsonSerializer.Serialize(value); + await _cacheDatabase.StringSetAsync(key, json, expiry); + _logger.LogInformation($"Cache item set with key: {key}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error setting cache item with key {key}"); + throw; + } + } + + /// + /// Removes a cache item by its key. + /// + /// The cache key. + public async ValueTask RemoveAsync(string key) + { + try + { + await _cacheDatabase.KeyDeleteAsync(key); + _logger.LogInformation($"Cache item removed with key: {key}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error removing cache item with key {key}"); + throw; + } + } + + /// + /// Checks if a cache item exists for the specified key. + /// + /// The cache key. + /// True if the cache item exists; otherwise, false. + public async ValueTask ExistsAsync(string key) + { + try + { + var exists = await _cacheDatabase.KeyExistsAsync(key); + _logger.LogInformation($"Cache item exists check for key: {key} - {exists}"); + return exists; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error checking existence of cache item with key {key}"); + throw; + } + } + + /// + /// Refreshes the expiration time of a cache item if it exists. + /// + /// The cache key. + /// The new expiration time for the cache item. + public async ValueTask RefreshAsync(string key, TimeSpan? expiry = null) + { + try + { + 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) + { + _logger.LogError(ex, $"Error refreshing cache item with key {key}"); + throw; + } + } + } +} diff --git a/Core.Blueprint.SQLServer/Adapters/BaseSQLAdapter.cs b/Core.Blueprint.SQLServer/Adapters/BaseSQLAdapter.cs new file mode 100644 index 0000000..9f1e438 --- /dev/null +++ b/Core.Blueprint.SQLServer/Adapters/BaseSQLAdapter.cs @@ -0,0 +1,65 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.SQLServer.Entities +{ + /// + /// Represents the base class for SQL Server entities, providing common properties for auditing and state management. + /// + public abstract class BaseSQLAdapter : IBaseSQLAdapter + { + /// + /// Gets or sets the identifier for the entity. + /// + [Key] + [JsonPropertyName("id")] + public int Id { get; init; } + + /// + /// Gets or sets the unique identifier for the entity. + /// + [JsonPropertyName("guid")] + public string Guid { get; init; } + + /// + /// Gets or sets the timestamp when the entity was created. + /// Default value is the current UTC time at the moment of instantiation. + /// + [JsonPropertyName("createdAt")] + public DateTime? CreatedAt { get; init; } + + /// + /// Gets or sets the identifier of the user or system that created the entity. + /// + [JsonPropertyName("createdBy")] + public string? CreatedBy { get; set; } + + /// + /// Gets or sets the timestamp when the entity was last updated. + /// Null if the entity has not been updated. + /// + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } + + /// + /// Gets or sets the identifier of the user or system that last updated the entity. + /// Null if the entity has not been updated. + /// + [JsonPropertyName("updatedBy")] + public string? UpdatedBy { get; set; } + + /// + /// Gets or sets the status of the entity, indicating whether it is active, inactive, or in another state. + /// Default value is . + /// + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public StatusEnum Status { get; set; } + + protected BaseSQLAdapter() + { + Guid = System.Guid.NewGuid().ToString(); + CreatedAt = DateTime.UtcNow; + } + } +} diff --git a/Core.Blueprint.SQLServer/Adapters/StatusEnum.cs b/Core.Blueprint.SQLServer/Adapters/StatusEnum.cs new file mode 100644 index 0000000..a7a72c8 --- /dev/null +++ b/Core.Blueprint.SQLServer/Adapters/StatusEnum.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Core.Blueprint.SQLServer.Entities +{ + /// + /// Defines the possible statuses for entities in the system. + /// Used to track the state of an entity, such as whether it is active, inactive, or deleted. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum StatusEnum + { + /// + /// Indicates that the entity is currently active and operational. + /// + Active = 0, + + /// + /// Indicates that the entity is currently inactive but still exists in the system. + /// Typically used for temporary deactivation or soft-offline states. + /// + Inactive = 1, + + /// + /// Indicates that the entity has been deleted and is no longer accessible. + /// Often used in soft-delete scenarios where the entity is retained for archival or audit purposes. + /// + Deleted = 2 + } +} diff --git a/Core.Blueprint.SQLServer/Configuration/RegisterBlueprint.cs b/Core.Blueprint.SQLServer/Configuration/RegisterBlueprint.cs new file mode 100644 index 0000000..76d6af6 --- /dev/null +++ b/Core.Blueprint.SQLServer/Configuration/RegisterBlueprint.cs @@ -0,0 +1,33 @@ +using Azure.Identity; +using Core.Blueprint.DAL.SQLServer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Core.Blueprint.SQLServer.Configuration +{ + /// + /// Provides extension methods for configuring SQL Server. + /// + public static class RegisterBlueprint + { + /// + /// Configures SQL Server services, including the database context and generic repository, for dependency injection. + /// + /// The service collection to which the SQL Server services will be added. + /// The application configuration object for accessing settings such as connection strings. + /// An updated with SQL Server services registered. + public static IServiceCollection AddSQLServer(this IServiceCollection services, IConfiguration configuration) + { + var chainedCredentials = new ChainedTokenCredential( + new ManagedIdentityCredential(), + new SharedTokenCacheCredential(), + new VisualStudioCredential(), + new VisualStudioCodeCredential() + ); + + services.AddScoped(typeof(IEntityRepository<,>), typeof(EntityRepository<,>)); + + return services; + } + } +} diff --git a/Core.Blueprint.SQLServer/Contracts/IBaseSQLAdapter.cs b/Core.Blueprint.SQLServer/Contracts/IBaseSQLAdapter.cs new file mode 100644 index 0000000..3b37b6a --- /dev/null +++ b/Core.Blueprint.SQLServer/Contracts/IBaseSQLAdapter.cs @@ -0,0 +1,57 @@ +using Core.Blueprint.SQLServer.Entities; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Core.Blueprint.SQLServer +{ + /// + /// Defines the interface for SQL Server entities, providing common properties for auditing and state management. + /// + public interface IBaseSQLAdapter + { + + /// + /// Gets or sets the identifier for the entity. + /// + [Key] + [JsonPropertyName("id")] + int Id { get; } + + /// + /// Gets or sets the GUID for the entity. + /// + [JsonPropertyName("guid")] + string Guid { get; } + + /// + /// Gets or sets the timestamp when the entity was created. + /// + [JsonPropertyName("createdAt")] + DateTime? CreatedAt { get; } + + /// + /// Gets or sets the identifier of the user or system that created the entity. + /// + [JsonPropertyName("createdBy")] + string? CreatedBy { get; set; } + + /// + /// Gets or sets the timestamp when the entity was last updated. + /// + [JsonPropertyName("updatedAt")] + DateTime? UpdatedAt { get; set; } + + /// + /// Gets or sets the identifier of the user or system that last updated the entity. + /// + [JsonPropertyName("updatedBy")] + string? UpdatedBy { get; set; } + + /// + /// Gets or sets the status of the entity, indicating whether it is active, inactive, or in another state. + /// + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + StatusEnum Status { get; set; } + } +} diff --git a/Core.Blueprint.SQLServer/Contracts/IEntityRepository.cs b/Core.Blueprint.SQLServer/Contracts/IEntityRepository.cs new file mode 100644 index 0000000..865c1fc --- /dev/null +++ b/Core.Blueprint.SQLServer/Contracts/IEntityRepository.cs @@ -0,0 +1,108 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Core.Blueprint.DAL.SQLServer +{ + /// + /// Defines the contract for a generic repository to manage entities in a SQL Server database. + /// + /// The type of the entity managed by the repository. Must be a class. + /// The type of the database context used by the repository. Must inherit from . + public interface IEntityRepository + where TEntity : class + where TContext : DbContext + { + /// + /// Retrieves all entities of type from the database. + /// + /// A task representing the asynchronous operation, with a collection of entities as the result. + Task> GetAllAsync(); + + /// + /// Retrieves all entities of type from the database that match a specified condition. + /// + /// An expression to filter the entities. + /// A task representing the asynchronous operation, with a collection of matching entities as the result. + Task> GetByConditionAsync(Expression> predicate); + + /// + /// Retrieves a single entity of type by its identifier. + /// + /// The identifier of the entity to retrieve. + /// A task representing the asynchronous operation, with the entity as the result, or null if not found. + Task GetByIdAsync(int id); + + /// + /// Retrieves the first entity of type that matches a specified condition, or null if no match is found. + /// + /// An expression to filter the entities. + /// A task representing the asynchronous operation, with the matching entity as the result, or null if none match. + Task FirstOrDefaultAsync(Expression> predicate); + + /// + /// Adds a new entity of type to the database. + /// + /// The entity to add. + /// A task representing the asynchronous operation. + Task AddAsync(TEntity entity); + + /// + /// Adds multiple entities of type to the database. + /// + /// The collection of entities to add. + /// A task representing the asynchronous operation. + Task AddRangeAsync(IEnumerable entities); + + /// + /// Updates an existing entity of type in the database. + /// + /// The entity to update. + /// The updated entity. + TEntity Update(TEntity entity); + + /// + /// Updates multiple entities of type in the database. + /// + /// The collection of entities to update. + void UpdateRange(IEnumerable entities); + + /// + /// Deletes an entity of type from the database. + /// + /// The entity to delete. + void Delete(TEntity entity); + + /// + /// Deletes multiple entities of type from the database. + /// + /// The collection of entities to delete. + void DeleteRange(IEnumerable entities); + + /// + /// Determines whether any entities of type exist in the database that match a specified condition. + /// + /// An expression to filter the entities. + /// A task representing the asynchronous operation, with a boolean result indicating whether any match exists. + Task AnyAsync(Expression> predicate); + + /// + /// Executes a raw SQL query and maps the result to entities of type . + /// + /// The raw SQL query to execute. + /// Optional parameters for the SQL query. + /// A task representing the asynchronous operation, with a collection of entities as the result. + Task> ExecuteRawSqlAsync(string sql, params object[] parameters); + + /// + /// Counts the total number of entities of type in the database. + /// + /// A task representing the asynchronous operation, with the count as the result. + Task CountAsync(); + + /// + /// Saves all pending changes to the database. + /// + /// A task representing the asynchronous operation. + Task SaveAsync(); + } +} diff --git a/Core.Blueprint.SQLServer/Core.Blueprint.SQLServer.csproj b/Core.Blueprint.SQLServer/Core.Blueprint.SQLServer.csproj new file mode 100644 index 0000000..7dc396d --- /dev/null +++ b/Core.Blueprint.SQLServer/Core.Blueprint.SQLServer.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/Core.Blueprint.SQLServer/Repositories/EntityRepository.cs b/Core.Blueprint.SQLServer/Repositories/EntityRepository.cs new file mode 100644 index 0000000..f87f62b --- /dev/null +++ b/Core.Blueprint.SQLServer/Repositories/EntityRepository.cs @@ -0,0 +1,182 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Core.Blueprint.DAL.SQLServer +{ + /// + /// The class provides a comprehensive generic repository + /// for managing entities using Entity Framework Core with SQL Server as the underlying database. + /// Designed as a package for consumption by external applications. + /// + /// The entity type managed by the repository. Must be a class. + /// The database context type. Must inherit from . + public class EntityRepository : IEntityRepository + where TEntity : class + where TContext : DbContext + { + private readonly TContext _context; + private readonly DbSet _dbSet; + + /// + /// Initializes a new instance of the class with a specified database context. + /// + /// The for database operations. + public EntityRepository(TContext context) + { + _context = context; + _dbSet = _context.Set(); + } + + /// + /// Retrieves all entities of type from the database. + /// + /// A task representing the asynchronous operation, with a list of entities as the result. + public async Task> GetAllAsync() + { + return await _dbSet.ToListAsync(); + } + + /// + /// Retrieves all entities of type from the database that match a specified filter. + /// + /// An expression to filter entities. + /// A task representing the asynchronous operation, with a list of filtered entities as the result. + public async Task> GetByConditionAsync(Expression> predicate) + { + return await _dbSet.Where(predicate).ToListAsync(); + } + + /// + /// Retrieves a single entity of type by its identifier. + /// + /// The identifier of the entity. + /// A task representing the asynchronous operation, with the entity as the result, or null if not found. + public async Task GetByIdAsync(int id) + { + var existingEntity = await _dbSet.FindAsync(id); + + if (existingEntity != null) + { + _context.Entry(existingEntity).State = EntityState.Detached; + } + + return existingEntity; + } + + /// + /// Adds a new entity to the database. + /// + /// The entity to add. + /// A task representing the asynchronous operation. + public async Task AddAsync(TEntity entity) + { + await _dbSet.AddAsync(entity); + } + + /// + /// Adds multiple entities to the database. + /// + /// The collection of entities to add. + /// A task representing the asynchronous operation. + public async Task AddRangeAsync(IEnumerable entities) + { + await _dbSet.AddRangeAsync(entities); + } + + /// + /// Updates an existing entity in the database. + /// + /// The entity to update. + /// The updated entity. + public TEntity Update(TEntity entity) + { + _dbSet.Attach(entity); + _context.Entry(entity).State = EntityState.Modified; + return entity; + } + + /// + /// Updates multiple entities in the database. + /// + /// The collection of entities to update. + public void UpdateRange(IEnumerable entities) + { + foreach (var entity in entities) + { + _dbSet.Attach(entity); + _context.Entry(entity).State = EntityState.Modified; + } + } + + /// + /// Deletes an entity from the database. + /// + /// The entity to delete. + public void Delete(TEntity entity) + { + if (_context.Entry(entity).State == EntityState.Detached) + { + _dbSet.Attach(entity); + } + _dbSet.Remove(entity); + } + + /// + /// Deletes multiple entities from the database. + /// + /// The collection of entities to delete. + public void DeleteRange(IEnumerable entities) + { + _dbSet.RemoveRange(entities); + } + + /// + /// Retrieves the first entity matching the specified condition or null if no match is found. + /// + /// An expression to filter entities. + /// A task representing the asynchronous operation, with the matched entity as the result. + public async Task FirstOrDefaultAsync(Expression> predicate) + { + return await _dbSet.FirstOrDefaultAsync(predicate); + } + + /// + /// Determines if any entities exist that match the specified condition. + /// + /// An expression to filter entities. + /// A task representing the asynchronous operation, with a boolean result indicating existence. + public async Task AnyAsync(Expression> predicate) + { + return await _dbSet.AnyAsync(predicate); + } + + /// + /// Saves all pending changes to the database. + /// + /// A task representing the asynchronous operation. + public async Task SaveAsync() + { + await _context.SaveChangesAsync(); + } + + /// + /// Executes a raw SQL query and maps the result to the specified entity type. + /// + /// The raw SQL query. + /// Optional parameters for the query. + /// An representing the result set. + public async Task> ExecuteRawSqlAsync(string sql, params object[] parameters) + { + return await _dbSet.FromSqlRaw(sql, parameters).ToListAsync(); + } + + /// + /// Counts the total number of entities in the database. + /// + /// A task representing the asynchronous operation, with the count as the result. + public async Task CountAsync() + { + return await _dbSet.CountAsync(); + } + } +} diff --git a/Core.Blueprint.Storage/Adapters/BlobAddDto.cs b/Core.Blueprint.Storage/Adapters/BlobAddDto.cs new file mode 100644 index 0000000..2040feb --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/BlobAddDto.cs @@ -0,0 +1,8 @@ +namespace Core.Blueprint.Storage +{ + public class BlobAddDto + { + public string? FileName { get; set; } + public byte[] FileContent { get; set; } = null!; + } +} diff --git a/Core.Blueprint.Storage/Adapters/BlobDownloadAdapter.cs b/Core.Blueprint.Storage/Adapters/BlobDownloadAdapter.cs new file mode 100644 index 0000000..0fbed6c --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/BlobDownloadAdapter.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Core.Blueprint.Storage.Adapters +{ + class BlobDownloadAdapter + { + } +} diff --git a/Core.Blueprint.Storage/Adapters/BlobDownloadUriAdapter.cs b/Core.Blueprint.Storage/Adapters/BlobDownloadUriAdapter.cs new file mode 100644 index 0000000..f7e0716 --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/BlobDownloadUriAdapter.cs @@ -0,0 +1,9 @@ +namespace Core.Blueprint.Storage.Adapters +{ + public class BlobDownloadUriAdapter + { + public Uri Uri { get; set; } = null!; + public string Name { get; set; } = null!; + public string? Status { get; set; } + } +} diff --git a/Core.Blueprint.Storage/Adapters/BlobFileAdapter.cs b/Core.Blueprint.Storage/Adapters/BlobFileAdapter.cs new file mode 100644 index 0000000..3c08dab --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/BlobFileAdapter.cs @@ -0,0 +1,13 @@ +namespace Core.Blueprint.Storage +{ + public class BlobFileAdapter + { + public string? Uri { get; set; } + public string Name { get; set; } = null!; + public string? DateUpload { get; set; } + public string? ContentType { get; set; } + public long? Size { get; set; } + public string? Status { get; set; } + public string? ShortDate { get; set; } + } +} diff --git a/Core.Blueprint.Storage/Adapters/BlobStorageAdapter.cs b/Core.Blueprint.Storage/Adapters/BlobStorageAdapter.cs new file mode 100644 index 0000000..be398da --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/BlobStorageAdapter.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Core.Blueprint.Storage +{ + public class BlobStorageAdapter + { + public string FileName { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string DownloadUrl { get; set; } = string.Empty; + } +} diff --git a/Core.Blueprint.Storage/Adapters/BlobStorageFolder.cs b/Core.Blueprint.Storage/Adapters/BlobStorageFolder.cs new file mode 100644 index 0000000..a04262f --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/BlobStorageFolder.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Core.Blueprint.Storage.Adapters +{ + public record BlobStorageFolder + { + public string Name { get; set; } + public List SubFolders { get; set; } = []; + public List Files { get; set; } = []; + } + public record BlobStorageFilesAdapter(string Content, string Name, string ContentType, string DownloadUrl); +} diff --git a/Core.Blueprint.Storage/Adapters/Trie/TrieNode.cs b/Core.Blueprint.Storage/Adapters/Trie/TrieNode.cs new file mode 100644 index 0000000..09c1498 --- /dev/null +++ b/Core.Blueprint.Storage/Adapters/Trie/TrieNode.cs @@ -0,0 +1,66 @@ + +namespace Core.Blueprint.Storage +{ + public class TrieNode + { + public Dictionary Children { get; private set; } + public bool IsEndOfWord { get; set; } + + public TrieNode() + { + Children = []; + IsEndOfWord = false; + } + } + public class Trie + { + private readonly TrieNode _root; + + public Trie() + { + _root = new TrieNode(); + } + + public void Insert(string word) + { + var node = _root; + foreach (var ch in word) + { + if (!node.Children.ContainsKey(ch)) + { + node.Children[ch] = new TrieNode(); + } + node = node.Children[ch]; + } + node.IsEndOfWord = true; + } + + public List SearchByPrefix(string? prefix) + { + var results = new List(); + var node = _root; + foreach (var ch in prefix) + { + if (!node.Children.ContainsKey(ch)) + { + return results; + } + node = node.Children[ch]; + } + SearchByPrefixHelper(node, prefix, results); + return results; + } + + private void SearchByPrefixHelper(TrieNode node, string currentPrefix, List results) + { + if (node.IsEndOfWord) + { + results.Add(currentPrefix); + } + foreach (var kvp in node.Children) + { + SearchByPrefixHelper(kvp.Value, currentPrefix + kvp.Key, results); + } + } + } +} diff --git a/Core.Blueprint.Storage/Configuration/RegisterBlueprint.cs b/Core.Blueprint.Storage/Configuration/RegisterBlueprint.cs new file mode 100644 index 0000000..9a98bec --- /dev/null +++ b/Core.Blueprint.Storage/Configuration/RegisterBlueprint.cs @@ -0,0 +1,38 @@ +using Azure.Identity; +using Core.Blueprint.Storage.Contracts; +using Core.Blueprint.Storage.Provider; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Core.Blueprint.Storage.Configuration +{ + public static class RegisterBlueprint + { + public static IServiceCollection AddBlobStorage(this IServiceCollection services, IConfiguration configuration) + { + + var blobConnection = configuration.GetConnectionString("BlobStorage"); + + if (blobConnection == null || 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() + ); + services.AddAzureClients(cfg => + { + cfg.AddBlobServiceClient(new Uri(blobConnection)).WithCredential(chainedCredentials); + }); + + services.AddScoped(); + + return services; + } + } +} \ No newline at end of file diff --git a/Core.Blueprint.Storage/Contracts/IBlobStorageProvider.cs b/Core.Blueprint.Storage/Contracts/IBlobStorageProvider.cs new file mode 100644 index 0000000..f47a099 --- /dev/null +++ b/Core.Blueprint.Storage/Contracts/IBlobStorageProvider.cs @@ -0,0 +1,181 @@ +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Sas; +using Core.Blueprint.Storage.Adapters; + +namespace Core.Blueprint.Storage.Contracts +{ + /// + /// Defines a contract for managing blobs and containers in Azure Blob Storage. + /// + public interface IBlobStorageProvider + { + /// + /// Creates the blob container if it does not exist. + /// + /// A containing the container information. + Task> CreateIfNotExistsAsync(); + + /// + /// Deletes the blob container if it exists. + /// + Task DeleteIfExistsAsync(); + + /// + /// Gets properties of the blob container. + /// + /// A containing container properties. + Task> GetPropertiesAsync(); + + /// + /// Sets metadata for the blob container. + /// + /// The metadata to set for the container. + Task SetMetadataAsync(IDictionary metadata); + + /// + /// Uploads a blob to the container. + /// + /// The name of the blob. + /// The content to upload. + /// A containing blob content information. + Task> UploadBlobAsync(string blobName, Stream content); + + /// + /// Downloads a blob from the container. + /// + /// The name of the blob. + /// A containing blob download information. + /// Thrown if the blob does not exist. + Task> DownloadBlobAsync(string blobName); + + /// + /// Deletes a blob from the container. + /// + /// The name of the blob. + Task DeleteBlobAsync(string blobName); + + /// + /// Lists all blobs in the container with an optional prefix. + /// + /// The prefix to filter blobs. + /// A collection of . + Task> ListBlobItemAsync(string? prefix = null); + + /// + /// Retrieves the account information for the associated Blob Service Client. + /// + /// + /// A that can be used to cancel the operation. + /// + /// + /// A task that represents the asynchronous operation. The task result contains the + /// object, which provides details about the account, such as the SKU + /// and account kind. + Task GetAccountInfoAsync(CancellationToken cancellation); + + /// + /// Gets a blob client for a specific blob. + /// + /// The name of the blob. + /// A for the blob. + BlobClient GetBlobClient(string blobName); + + /// + /// Lists blobs hierarchically using a delimiter. + /// + /// The prefix to filter blobs. + /// The delimiter to use for hierarchy. + /// A collection of . + Task> ListBlobsByHierarchyAsync(string? prefix = null, string delimiter = "/"); + + /// + /// Generates a SAS token for the container with specified permissions. + /// + /// The permissions to assign to the SAS token. + /// The expiration time for the SAS token. + /// A containing the SAS token. + /// Thrown if SAS URI generation is not supported. + Uri GenerateContainerSasUri(BlobContainerSasPermissions permissions, DateTimeOffset expiresOn); + + /// + /// Acquires a lease on the blob container. + /// + /// The optional proposed lease ID. + /// The optional lease duration. + /// A containing lease information. + Task> AcquireLeaseAsync(string? proposedId = null, TimeSpan? duration = null); + + /// + /// Releases a lease on the blob container. + /// + /// The lease ID to release. + Task ReleaseLeaseAsync(string leaseId); + + /// + /// Sets access policies for the blob container. + /// + /// The type of public access to allow. + /// The optional list of signed identifiers for access policy. + Task SetAccessPolicyAsync(PublicAccessType accessType, IEnumerable? identifiers = null); + + /// + /// Lists blobs in the container with an optional prefix. + /// + /// The prefix to filter blobs. + /// A collection of . + Task> ListBlobsAsync(string? prefix = null); + + /// + /// Uploads a blob to the container. + /// + /// The blob to upload. + /// A representing the uploaded blob. + Task UploadBlobAsync(BlobAddDto newBlob); + + /// + /// Deletes a blob from the container. + /// + /// The name of the blob to delete. + /// A representing the deleted blob, or null if the blob was not found. + Task DeleteBlobsAsync(string fileName); + + /// + /// Downloads a blob's content. + /// + /// The name of the blob. + /// A representing the downloaded blob. + /// Thrown if the blob does not exist. + Task DownloadBlobsAsync(string blobName); + + /// + /// Generates a secure download URI for a specified blob in the storage container. + /// + /// The name of the blob for which the download URI is being generated. + /// + /// An instance of containing the generated URI, blob name, and status. + /// + /// + /// The generated URI includes a Shared Access Signature (SAS) token, which allows secure, time-limited access to the blob. + /// The SAS token grants read-only access to the blob for a duration of 5 minutes starting from the current time. + /// + /// Thrown if is null or empty. + /// Thrown if there is an issue communicating with the Azure Blob service. + BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName); + + /// + /// Retrieves the hierarchical folder structure. + /// + /// The prefix to start the hierarchy retrieval. + /// A list of representing the folder structure. + Task> GetFolderHierarchyAsync(string prefix); + + /// + /// Lists neighboring folders based on a prefix. + /// + /// The prefix to search for neighboring folders. + /// A dictionary grouping folder names by their prefix. + Task>> ListNeighborFoldersAsync(string? prefix); + } +} \ No newline at end of file diff --git a/Core.Blueprint.Storage/Core.Blueprint.Storage.csproj b/Core.Blueprint.Storage/Core.Blueprint.Storage.csproj new file mode 100644 index 0000000..e892e7d --- /dev/null +++ b/Core.Blueprint.Storage/Core.Blueprint.Storage.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Core.Blueprint.Storage/Provider/BlobStorageProvider.cs b/Core.Blueprint.Storage/Provider/BlobStorageProvider.cs new file mode 100644 index 0000000..5069a42 --- /dev/null +++ b/Core.Blueprint.Storage/Provider/BlobStorageProvider.cs @@ -0,0 +1,372 @@ +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; +using Azure.Storage.Sas; +using Core.Blueprint.Storage.Adapters; +using Core.Blueprint.Storage.Contracts; +using Microsoft.Extensions.Configuration; + +namespace Core.Blueprint.Storage.Provider +{ + public sealed class BlobStorageProvider : IBlobStorageProvider + { + private readonly BlobServiceClient _blobServiceClient; + private readonly BlobContainerClient _blobContainerClient; + private readonly string _containerName; + private readonly Trie _trie = new Trie(); + + public BlobStorageProvider(BlobServiceClient blobServiceClient, IConfiguration configuration) + { + _blobServiceClient = blobServiceClient; + _containerName = configuration.GetSection("BlobStorage:ContainerName").Value ?? ""; + + if (string.IsNullOrEmpty(_containerName)) + throw new ArgumentException("Blob container cannot be null or empty."); + + _blobContainerClient = blobServiceClient.GetBlobContainerClient(_containerName); + } + + /// + /// Creates the blob container if it does not exist. + /// + public async Task> CreateIfNotExistsAsync() + { + return await _blobContainerClient.CreateIfNotExistsAsync(); + } + + /// + /// Deletes the blob container if it exists. + /// + public async Task DeleteIfExistsAsync() + { + await _blobContainerClient.DeleteIfExistsAsync(); + } + + /// + /// Gets properties of the blob container. + /// + public async Task> GetPropertiesAsync() + { + return await _blobContainerClient.GetPropertiesAsync(); + } + + /// + /// Sets metadata for the blob container. + /// + public async Task SetMetadataAsync(IDictionary metadata) + { + await _blobContainerClient.SetMetadataAsync(metadata); + } + + /// + /// Uploads a blob to the container. + /// + public async Task> UploadBlobAsync(string blobName, Stream content) + { + var blobClient = _blobContainerClient.GetBlobClient(blobName); + return await blobClient.UploadAsync(content, overwrite: true); + } + + /// + /// Downloads a blob from the container. + /// + public async Task> DownloadBlobAsync(string blobName) + { + var blobClient = _blobContainerClient.GetBlobClient(blobName); + return await blobClient.DownloadAsync(); + } + + /// + /// Deletes a blob from the container. + /// + public async Task DeleteBlobAsync(string blobName) + { + var blobClient = _blobContainerClient.GetBlobClient(blobName); + return await blobClient.DeleteIfExistsAsync(); + } + + /// + /// Lists all blobs in the container with an optional prefix. + /// + public async Task> ListBlobItemAsync(string? prefix = null) + { + var blobs = new List(); + + await foreach (var blobItem in _blobContainerClient.GetBlobsAsync(prefix: prefix)) + { + blobs.Add(blobItem); + } + + return blobs; + } + + /// + /// Retrieves the account information for the associated Blob Service Client. + /// + /// + /// A that can be used to cancel the operation. + /// + /// + /// A task that represents the asynchronous operation. The task result contains the + /// object, which provides details about the account, such as the SKU + /// and account kind. + public async Task GetAccountInfoAsync(CancellationToken cancellation) + { + return await _blobServiceClient.GetAccountInfoAsync(cancellation); + } + + /// + /// Gets a blob client for a specific blob. + /// + public BlobClient GetBlobClient(string blobName) + { + return _blobContainerClient.GetBlobClient(blobName); + } + + /// + /// Lists blobs hierarchically using a delimiter. + /// + public async Task> ListBlobsByHierarchyAsync(string? prefix = null, string delimiter = "/") + { + var blobs = new List(); + + await foreach (var blobHierarchyItem in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: delimiter)) + { + blobs.Add(blobHierarchyItem); + } + + return blobs; + } + + /// + /// Generates a SAS token for the container with specified permissions. + /// + public Uri GenerateContainerSasUri(BlobContainerSasPermissions permissions, DateTimeOffset expiresOn) + { + if (!_blobContainerClient.CanGenerateSasUri) + { + throw new InvalidOperationException("Cannot generate SAS URI. Ensure the client is authorized with account key credentials."); + } + + var sasBuilder = new BlobSasBuilder + { + BlobContainerName = _blobContainerClient.Name, + Resource = "c", // c for container + ExpiresOn = expiresOn + }; + + sasBuilder.SetPermissions(permissions); + return _blobContainerClient.GenerateSasUri(sasBuilder); + } + + /// + /// Acquires a lease on the blob container. + /// + public async Task> AcquireLeaseAsync(string? proposedId = null, TimeSpan? duration = null) + { + return await _blobContainerClient.GetBlobLeaseClient(proposedId).AcquireAsync(duration ?? TimeSpan.FromSeconds(60)); + } + + /// + /// Releases a lease on the blob container. + /// + public async Task ReleaseLeaseAsync(string leaseId) + { + await _blobContainerClient.GetBlobLeaseClient(leaseId).ReleaseAsync(); + } + + /// + /// Sets access policies for the blob container. + /// + public async Task SetAccessPolicyAsync(PublicAccessType accessType, IEnumerable? identifiers = null) + { + await _blobContainerClient.SetAccessPolicyAsync(accessType, identifiers); + } + /// + /// Lists blobs in the container with an optional prefix. + /// + public async Task> ListBlobsAsync(string? prefix = null) + { + var blobs = new List(); + + await foreach (BlobItem blob in _blobContainerClient.GetBlobsAsync(prefix: prefix)) + { + blobs.Add(new BlobFileAdapter + { + Name = blob.Name, + Uri = $"{_blobContainerClient.Uri}/{blob.Name}", + ContentType = blob.Properties.ContentType, + Size = blob.Properties.ContentLength, + DateUpload = blob.Properties.LastModified?.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss"), + ShortDate = blob.Properties.LastModified?.UtcDateTime.ToString("MM-dd-yyyy"), + Status = "Available" + }); + } + + return blobs; + } + + /// + /// Uploads a blob to the container. + /// + public async Task UploadBlobAsync(BlobAddDto newBlob) + { + var blobClient = _blobContainerClient.GetBlobClient(newBlob.FileName); + + using var stream = new MemoryStream(newBlob.FileContent); + await blobClient.UploadAsync(stream, overwrite: true); + + var properties = await blobClient.GetPropertiesAsync(); + + return new BlobFileAdapter + { + Name = newBlob.FileName ?? "", + Uri = blobClient.Uri.ToString(), + ContentType = properties.Value.ContentType, + Size = properties.Value.ContentLength, + DateUpload = properties.Value.LastModified.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss"), + ShortDate = properties.Value.LastModified.UtcDateTime.ToString("MM-dd-yyyy"), + Status = "Uploaded" + }; + } + + /// + /// Deletes a blob from the container. + /// + public async Task DeleteBlobsAsync(string fileName) + { + var blobClient = _blobContainerClient.GetBlobClient(fileName); + + if (await blobClient.ExistsAsync()) + { + var properties = await blobClient.GetPropertiesAsync(); + var _response = await blobClient.DeleteIfExistsAsync(); + + return new BlobFileAdapter + { + Name = fileName, + Uri = blobClient.Uri.ToString(), + ContentType = properties.Value.ContentType, + Size = properties.Value.ContentLength, + DateUpload = properties.Value.LastModified.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss"), + ShortDate = properties.Value.LastModified.UtcDateTime.ToString("MM-dd-yyyy"), + Status = _response ? "Deleted" : "Failed to delete" + }; + } + + return null; + } + + /// + /// Downloads a blob's content. + /// + public async Task DownloadBlobsAsync(string blobName) + { + var blobClient = _blobContainerClient.GetBlobClient(blobName); + + if (!await blobClient.ExistsAsync()) + { + throw new FileNotFoundException($"Blob '{blobName}' does not exist in the container '{_containerName}'."); + } + + return await blobClient.DownloadAsync(); + } + + /// + /// Generates a secure download URI for a specified blob in the storage container. + /// + /// The name of the blob for which the download URI is being generated. + /// + /// An instance of containing the generated URI, blob name, and status. + /// + /// + /// The generated URI includes a Shared Access Signature (SAS) token, which allows secure, time-limited access to the blob. + /// The SAS token grants read-only access to the blob for a duration of 5 minutes starting from the current time. + /// + /// Thrown if is null or empty. + /// Thrown if there is an issue communicating with the Azure Blob service. + public BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName) + { + var delegationKey = _blobServiceClient.GetUserDelegationKey(DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddHours(2)); + + var blob = _blobContainerClient.GetBlobClient(blobName); + + var sasBuilder = new BlobSasBuilder() + { + BlobContainerName = blob.BlobContainerName, + BlobName = blob.Name, + Resource = "b", + StartsOn = DateTimeOffset.UtcNow, + ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5), + }; + sasBuilder.SetPermissions(BlobAccountSasPermissions.Read); + sasBuilder.Protocol = SasProtocol.Https; + + var blobUriBuilder = new BlobUriBuilder(blob.Uri) + { + Sas = sasBuilder.ToSasQueryParameters(delegationKey, _blobServiceClient.AccountName) + }; + + return new BlobDownloadUriAdapter + { + Uri = blobUriBuilder.ToUri(), + Name = blob.Name, + Status = "Available" + }; + } + + /// + /// Retrieves the hierarchical folder structure. + /// + public async Task> GetFolderHierarchyAsync(string prefix) + { + var rootFolder = new BlobStorageFolder { Name = prefix }; + await PopulateFolderAsync(rootFolder, prefix); + return new List { rootFolder }; + } + + private async Task PopulateFolderAsync(BlobStorageFolder folder, string? prefix) + { + await foreach (var blobHierarchy in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: "/")) + { + if (blobHierarchy.IsPrefix) + { + var subFolder = new BlobStorageFolder { Name = blobHierarchy.Prefix.TrimEnd('/') }; + folder.SubFolders.Add(subFolder); + await PopulateFolderAsync(subFolder, blobHierarchy.Prefix); + } + else + { + folder.Files.Add(new BlobStorageFilesAdapter(Content: blobHierarchy.Prefix, //Fix + Name: blobHierarchy.Blob.Name, + ContentType: "", + DownloadUrl: $"{_blobContainerClient.Uri}/{blobHierarchy.Blob.Name}")); + } + } + } + public async Task>> ListNeighborFoldersAsync(string? prefix) + { + await ListFoldersInTrieAsync(prefix); + + var groupedFolders = _trie.SearchByPrefix(prefix) + .OrderBy(folder => folder) + .GroupBy(folder => folder.Substring(0, 1)) + .ToDictionary(group => group.Key, group => group.ToList()); + + return groupedFolders; + } + private async Task ListFoldersInTrieAsync(string? prefix) + { + await foreach (var blobHierarchy in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: "/")) + { + if (blobHierarchy.IsPrefix) + { + var folderName = blobHierarchy.Prefix.TrimEnd('/').Split('/').Last(); + _trie.Insert(folderName); + } + } + } + } +}