Add project files.

This commit is contained in:
Sergio Matias Urquin 2025-04-29 18:42:29 -06:00
parent 9c1958d351
commit 83fc1878c4
67 changed files with 4586 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1,9 @@

namespace Core.Blueprint.KeyVault
{
public sealed class KeyVaultRequest
{
public required string Name { get; set; }
public required string Value { get; set; }
}
}

View File

@ -0,0 +1,10 @@

namespace Core.Blueprint.KeyVault
{
public sealed class KeyVaultResponse
{
public string Name { get; set; } = null!;
public string Value { get; set; } = null!;
}
}

View File

@ -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
{
/// <summary>
/// Registers the SecretClient for Azure Key Vault as a singleton service.
/// </summary>
/// <param name="services">The IServiceCollection to add the services to.</param>
/// <param name="configuration">The application's configuration.</param>
/// <returns>The updated IServiceCollection.</returns>
/// <exception cref="ArgumentNullException">Thrown when the KeyVault URI is missing in the configuration.</exception>
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<IKeyVaultProvider, KeyVaultProvider>();
return services;
}
}
}

View File

@ -0,0 +1,48 @@

namespace Core.Blueprint.KeyVault
{
/// <summary>
/// Interface for managing secrets in Azure Key Vault.
/// </summary>
public interface IKeyVaultProvider
{
/// <summary>
/// Creates a new secret in Azure Key Vault.
/// </summary>
/// <param name="keyVaultRequest">The request containing the name and value of the secret.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>A <see cref="KeyVaultResponse"/> containing the details of the created secret.</returns>
ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken);
/// <summary>
/// Deletes a secret from Azure Key Vault if it exists.
/// </summary>
/// <param name="secretName">The name of the secret to delete.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>
/// A <see cref="Tuple"/> containing a status message and a boolean indicating whether the secret was successfully deleted.
/// </returns>
ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a secret from Azure Key Vault.
/// </summary>
/// <param name="secretName">The name of the secret to retrieve.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>
/// A <see cref="Tuple"/> containing the <see cref="KeyVaultResponse"/> with secret details
/// and an optional error message if the secret was not found.
/// </returns>
ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned.
/// </summary>
/// <param name="newSecret">The updated secret information.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>
/// A <see cref="Tuple"/> containing the updated <see cref="KeyVaultResponse"/> and an optional error message if the secret was not found.
/// </returns>
ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,93 @@
using Azure;
using Azure.Security.KeyVault.Secrets;
namespace Core.Blueprint.KeyVault
{
/// <summary>
/// Provides operations for managing secrets in Azure Key Vault.
/// </summary>
public sealed class KeyVaultProvider(SecretClient keyVaultProvider): IKeyVaultProvider
{
/// <summary>
/// Creates a new secret in Azure Key Vault.
/// </summary>
/// <param name="keyVaultRequest">The request containing the name and value of the secret.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>A <see cref="KeyVaultResponse"/> containing the details of the created secret.</returns>
public async ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken)
{
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;
}
/// <summary>
/// Deletes a secret from Azure Key Vault if it exists.
/// </summary>
/// <param name="secretName">The name of the secret to delete.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>
/// A <see cref="Tuple"/> containing a status message and a boolean indicating whether the secret was successfully deleted.
/// </returns>
public async ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken)
{
var existingSecret = await this.GetSecretAsync(secretName, cancellationToken);
if (existingSecret != null)
{
await keyVaultProvider.StartDeleteSecretAsync(secretName, cancellationToken);
return new("Key Deleted", true);
}
return new("Key Not Found", false);
}
/// <summary>
/// Retrieves a secret from Azure Key Vault.
/// </summary>
/// <param name="secretName">The name of the secret to retrieve.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>
/// A <see cref="Tuple"/> containing the <see cref="KeyVaultResponse"/> with secret details
/// and an optional error message if the secret was not found.
/// </returns>
public async ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken)
{
KeyVaultSecret azureResponse = await keyVaultProvider.GetSecretAsync(secretName, cancellationToken: cancellationToken);
if (azureResponse == null)
{
return new(new KeyVaultResponse(), "Key Not Found");
}
return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty);
}
/// <summary>
/// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned.
/// </summary>
/// <param name="newSecret">The updated secret information.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>
/// A <see cref="Tuple"/> containing the updated <see cref="KeyVaultResponse"/> and an optional error message if the secret was not found.
/// </returns>
public async ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken)
{
KeyVaultResponse _response = new();
var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken);
if (existingSecret == null)
{
return new(new KeyVaultResponse(), "Key Not Found");
}
KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(newSecret.Name, newSecret.Value), cancellationToken);
_response.Value = azureResponse.Value;
_response.Name = azureResponse.Name;
return new(new KeyVaultResponse { Name = newSecret.Name, Value = azureResponse.Value }, string.Empty);
}
}
}

View File

@ -0,0 +1,43 @@
// ***********************************************************************
// <copyright file="ErrorDetailsDto.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// The service error details transfer object.
/// </summary>
public class ErrorDetails
{
/// <summary>
/// Gets or sets the service error code.
/// </summary>
/// <example>healthy</example>
[DisplayName(DisplayNames.ErrorCode)]
[JsonPropertyName(DisplayNames.ErrorCode)]
public string? ErrorCode { get; set; }
/// <summary>
/// Gets or sets the service error message.
/// </summary>
/// <example>This is an example message.</example>
[DisplayName(DisplayNames.Message)]
[JsonPropertyName(DisplayNames.Message)]
public string? Message { get; set; }
/// <summary>
/// Gets or sets the service target.
/// </summary>
/// <example>healthy</example>
[DisplayName(DisplayNames.Target)]
[JsonPropertyName(DisplayNames.Target)]
public string? Target { get; set; }
}
}

View File

@ -0,0 +1,45 @@
// ***********************************************************************
// <copyright file="HttpErrorDto.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// The service HTTP error data transfer object.
/// </summary>
public class HttpError
{
/// <summary>
/// Gets or sets the error.
/// </summary>
[DisplayName(DisplayNames.Error)]
[JsonPropertyName(DisplayNames.Error)]
public ErrorDetails Error { get; set; }
/// <summary>
/// Creates a new instance of <see cref="HttpError{TMessage}"/>
/// with custom parameters.
/// </summary>
/// <param name="message">The HTTP error message.</param>
/// <param name="errorCode">The HTTP error code.</param>
/// <param name="target">The HTTP error target.</param>
public HttpError(
string? message,
string? errorCode,
string? target)
{
Error = new ErrorDetails
{
ErrorCode = errorCode,
Message = message,
Target = target,
};
}
}
}

View File

@ -0,0 +1,41 @@
// ***********************************************************************
// <copyright file="HttpException.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// The service HTTP exception.
/// Extends the <see cref="Exception"/> class.
/// </summary>
public class HttpException : Exception
{
/// <summary>
/// Gets or sets the exception error code.
/// </summary>
public string? ErrorCode { get; set; }
/// <summary>
/// Gets or sets the exception status code.
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// Creates a new instance of <see cref="HttpException"/>.
/// </summary>
/// <param name="statusCode">The exception status code.</param>
/// <param name="errorCode">The exception error code.</param>
/// <param name="message">The exception message.</param>
public HttpException(
int statusCode,
string errorCode,
string message)
: base(message)
{
ErrorCode = errorCode;
StatusCode = statusCode;
}
}
}

View File

@ -0,0 +1,120 @@
// ***********************************************************************
// <copyright file="LogDetail.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// The service logger detail object.
/// </summary>
/// <typeparam name="TMessage">The generic message type.</typeparam>
public class LogDetail<TMessage>
{
/// <summary>
/// Gets or sets the log severity.
/// </summary>
/// <example>info</example>
[DisplayName(DisplayNames.Severity)]
[JsonPropertyName(DisplayNames.Severity)]
public LogSeverity Severity { get; set; }
/// <summary>
/// Gets or sets the timestamp.
/// </summary>
[DisplayName(DisplayNames.Timestamp)]
[JsonPropertyName(DisplayNames.Timestamp)]
public DateTime Timestamp { get; set; }
/// <summary>
/// Gets or sets the environment.
/// </summary>
/// <example>Development</example>
[DisplayName(DisplayNames.Environment)]
[JsonPropertyName(DisplayNames.Environment)]
public string? Environment { get; set; }
/// <summary>
/// Gets or sets the target.
/// </summary>
[DisplayName(DisplayNames.Target)]
[JsonPropertyName(DisplayNames.Target)]
public LogTarget? Target { get; set; }
/// <summary>
/// Gets or sets the x-forwarded-for header.
/// </summary>
/// <example>localhost</example>
[DisplayName(DisplayNames.XForwardedFor)]
[JsonPropertyName(DisplayNames.XForwardedFor)]
public string? XForwardedFor { get; set; }
/// <summary>
/// Gets or sets the service identifier.
/// </summary>
/// <example><see cref="Guid.NewGuid()"/></example>
[DisplayName(DisplayNames.ServiceId)]
[JsonPropertyName(DisplayNames.ServiceId)]
public string? ServiceId { get; set; }
/// <summary>
/// Gets or sets the request identifier.
/// </summary>
/// <example><see cref="Guid.NewGuid()"/></example>
[DisplayName(DisplayNames.RequestId)]
[JsonPropertyName(DisplayNames.RequestId)]
public string? RequestId { get; set; }
/// <summary>
/// Gets or sets the keyVaultProvider identifier.
/// </summary>
/// <example><see cref="Guid.NewGuid()"/></example>
[DisplayName(DisplayNames.ClientId)]
[JsonPropertyName(DisplayNames.ClientId)]
public string? ClientId { get; set; }
/// <summary>
/// Gets or sets the keyVaultProvider identifier.
/// </summary>
/// <example>keyVaultProviderRequest</example>
[DisplayName(DisplayNames.Operation)]
[JsonPropertyName(DisplayNames.Operation)]
public LogOperation Operation { get; set; }
/// <summary>
/// Gets or sets the user name.
/// </summary>
/// <example>keyVaultProviderRequest</example>
[DisplayName(DisplayNames.User)]
[JsonPropertyName(DisplayNames.User)]
public string? User { get; set; }
/// <summary>
/// Gets or sets user's email.
/// </summary>
/// <example>keyVaultProviderRequest</example>
[DisplayName(DisplayNames.Email)]
[JsonPropertyName(DisplayNames.Email)]
public string? Email { get; set; }
/// <summary>
/// Gets or sets the user identifier.
/// </summary>
/// <example>keyVaultProviderRequest</example>
[DisplayName(DisplayNames.UserId)]
[JsonPropertyName(DisplayNames.UserId)]
public string? UserId { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
/// <example>A custom log message.</example>
[DisplayName(DisplayNames.Message)]
[JsonPropertyName(DisplayNames.Message)]
public TMessage? Message { get; set; }
}
}

View File

@ -0,0 +1,55 @@
// ***********************************************************************
// <copyright file="LogOperation.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// Represents all possible values for log operation.
/// </summary>
[DataContract]
public enum LogOperation
{
/// <summary>
/// The keyVaultProvider request log operation type.
/// </summary>
[EnumMember(Value = DisplayNames.ClientRequest)]
[JsonPropertyName(DisplayNames.ClientRequest)]
ClientRequest = 0,
/// <summary>
/// The keyVaultProvider response log operation type.
/// </summary>
[EnumMember(Value = DisplayNames.ClientResponse)]
ClientResponse = 1,
/// <summary>
/// The external request log operation type.
/// </summary>
[EnumMember(Value = DisplayNames.ExternalRequest)]
ExternalRequest = 2,
/// <summary>
/// The external response log operation type.
/// </summary>
[EnumMember(Value = DisplayNames.ExternalResponse)]
ExternalResponse = 3,
/// <summary>
/// The error log operation type.
/// </summary>
[EnumMember(Value = DisplayNames.Error)]
Error = 4,
/// <summary>
/// The info log operation type.
/// </summary>
[EnumMember(Value = DisplayNames.Info)]
Info = 5,
}
}

View File

@ -0,0 +1,41 @@
// ***********************************************************************
// <copyright file="LogSeverity.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.Runtime.Serialization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// Represents all possible values for log severity.
/// </summary>
[DataContract]
public enum LogSeverity
{
/// <summary>
/// The information severity level.
/// </summary>
[EnumMember(Value = DisplayNames.Information)]
Info = 0,
/// <summary>
/// The warning severity level.
/// </summary>
[EnumMember(Value = DisplayNames.Warning)]
Warn = 1,
/// <summary>
/// The error severity level.
/// </summary>
[EnumMember(Value = DisplayNames.Error)]
Error = 2,
/// <summary>
/// The fatal severity level.
/// </summary>
[EnumMember(Value = DisplayNames.Fatal)]
Fatal = 3,
}
}

View File

@ -0,0 +1,41 @@
// ***********************************************************************
// <copyright file="LogTarget.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// The service logger target object.
/// </summary>
public class LogTarget
{
/// <summary>
/// Gets or sets the log target method.
/// </summary>
/// <example>GET</example>
[DisplayName(DisplayNames.Method)]
[JsonPropertyName(DisplayNames.Method)]
public string? Method { get; set; }
/// <summary>
/// Gets or sets the log target host.
/// </summary>
/// <example>GET</example>
[DisplayName(DisplayNames.Host)]
[JsonPropertyName(DisplayNames.Host)]
public string? Host { get; set; }
/// <summary>
/// Gets or sets the log target route.
/// </summary>
/// <example>GET</example>
[DisplayName(DisplayNames.Route)]
[JsonPropertyName(DisplayNames.Route)]
public string? Route { get; set; }
}
}

View File

@ -0,0 +1,20 @@
// ***********************************************************************
// <copyright file="ServiceSettings.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// The service settings.
/// </summary>
public class ServiceSettings
{
/// <summary>
/// Gets or sets the service identifier.
/// </summary>
public string? ApplicationName { get; set; }
public string? LayerName { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Provides extension methods for configuring logging in the application.
/// </summary>
public static class Registerblueprint
{
/// <summary>
/// Registers logging services in the application.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> for accessing configuration and application setup.</param>
/// <returns>The updated <see cref="IServiceCollection"/>.</returns>
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<ILoggerProvider, LoggerProvider>();
return services;
}
/// <summary>
/// Configures middleware for logging and error handling in the application.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> used to configure the middleware pipeline.</param>
/// <param name="serviceSettings">The service settings required by the middleware.</param>
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);
}
/// <summary>
/// Adds middleware to handle HTTP exceptions globally.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <param name="settings">The settings used by the exception handler middleware.</param>
/// <returns>The updated <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseHttpExceptionHandler(this IApplicationBuilder builder, ServiceSettings settings)
{
return builder.UseMiddleware<HttpErrorMiddleware>(settings);
}
/// <summary>
/// Adds custom HTTP logging middleware to the application pipeline.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <param name="settings">The settings used by the logging middleware.</param>
/// <returns>The updated <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseCustomHttpLogging(this IApplicationBuilder builder, ServiceSettings settings)
{
return builder.UseMiddleware<HttpLoggingMiddleware>(settings);
}
}
}

View File

@ -0,0 +1,48 @@
// ***********************************************************************
// <copyright file="Claims.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// Constants for claims used in JWT tokens.
/// </summary>
public class Claims
{
/// <summary>
/// Claim name for user's name.
/// </summary>
public const string Name = "name";
/// <summary>
/// Claim name for user's name.
/// </summary>
public const string Email = "email";
/// <summary>
/// Claim name for user's ID.
/// </summary>
public const string Id = "id";
/// <summary>
/// Claim name for user's role ID.
/// </summary>
public const string Rol = "rol";
/// <summary>
/// Claim name for user's companies.
/// </summary>
public const string Companies = "companies";
/// <summary>
/// Claim name for user's projects.
/// </summary>
public const string Projects = "projects";
/// <summary>
/// Claim name for user's surveys.
/// </summary>
public const string Surveys = "surveys";
}
}

View File

@ -0,0 +1,397 @@
// ***********************************************************************
// <copyright file="DisplayNames.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// Constants of the display names for this service.
/// </summary>
public static class DisplayNames
{
/// <summary>
/// The active patameter.
/// </summary>
public const string Active = "active";
/// <summary>
/// The keyVaultProvider identifier parameter.
/// </summary>
public const string ClientId = "keyVaultProviderId";
/// <summary>
/// The keyVaultProvider request parameter.
/// </summary>
public const string ClientRequest = "keyVaultProviderRequest";
/// <summary>
/// The keyVaultProvider response parameter.
/// </summary>
public const string ClientResponse = "keyVaultProviderResponse";
/// <summary>
/// The creation date.
/// </summary>
public const string CreationDate = "creationDate";
/// <summary>
/// The content parameter.
/// </summary>
public const string Content = "content";
/// <summary>
/// The delete parameter.
/// </summary>
public const string Delete = "delete";
/// <summary>
/// The description parameter.
/// </summary>
public const string Description = "description";
/// <summary>
/// The detail parameter.
/// </summary>
public const string Detail = "detail";
/// <summary>
/// The environment parameter.
/// </summary>
public const string Environment = "environment";
/// <summary>
/// The error log severity level parameter.
/// </summary>
public const string Error = "error";
/// <summary>
/// The error code parameter.
/// </summary>
public const string ErrorCode = "errorCode";
/// <summary>
/// The external request parameter.
/// </summary>
public const string ExternalRequest = "externalRequest";
/// <summary>
/// The external response parameter.
/// </summary>
public const string ExternalResponse = "externalResponse";
/// <summary>
/// The fatal log severity level parameter.
/// </summary>
public const string Fatal = "fatal";
/// <summary>
/// The host parameter.
/// </summary>
public const string Host = "host";
/// <summary>
/// The identifier parameter.
/// </summary>
public const string Id = "id";
/// <summary>
/// The inactive parameter.
/// </summary>
public const string Inactive = "inactive";
/// <summary>
/// The info log severity level parameter.
/// </summary>
public const string Info = "info";
/// <summary>
/// The information log severity level parameter.
/// </summary>
public const string Information = "information";
/// <summary>
/// The media parameter.
/// </summary>
public const string Media = "media";
/// <summary>
/// The media type parameter.
/// </summary>
public const string MediaType = "mediaType";
/// <summary>
/// The media use type parameter.
/// </summary>
public const string MediaUseType = "mediaUseType";
/// <summary>
/// Th message parameter.
/// </summary>
public const string Message = "message";
/// <summary>
/// The method parameter.
/// </summary>
public const string Method = "method";
/// <summary>
/// The monday parameter.
/// </summary>
public const string Monday = "monday";
/// <summary>
/// The MXN parameter.
/// </summary>
public const string MXN = "MXN";
/// <summary>
/// The name parameter.
/// </summary>
public const string Name = "name";
/// <summary>
/// The next page parameter.
/// </summary>
public const string NextPage = "nextPage";
/// <summary>
/// The nick name parameter.
/// </summary>
public const string NickName = "nickName";
/// <summary>
/// The note parameter.
/// </summary>
public const string Note = "note";
/// <summary>
/// The not so affordable parameter.
/// </summary>
public const string NotSoAffordable = "notSoAffordable";
/// <summary>
/// The object status parameter.
/// </summary>
public const string ObjectStatus = "objectStatus";
/// <summary>
/// The opening time parameter.
/// </summary>
public const string OpeningTime = "openingTime";
/// <summary>
/// The operation days parameter.
/// </summary>
public const string OperationDays = "operationDays";
/// <summary>
/// The page parameter.
/// </summary>
public const string Page = "page";
/// <summary>
/// The page count parameter.
/// </summary>
public const string PageCount = "pageCount";
/// <summary>
/// The page metadata parameter.
/// </summary>
public const string PageMetadata = "pageMetadata";
/// <summary>
/// The page size parameter.
/// </summary>
public const string PageSize = "pageSize";
/// <summary>
/// The parent identifier parameter.
/// </summary>
public const string ParentId = "ParentId";
/// <summary>
/// The pet ticket price parameter.
/// </summary>
public const string PetTicketPrice = "petTicketPrice";
/// <summary>
/// The place parameter.
/// </summary>
public const string Place = "place";
/// <summary>
/// The place type parameter.
/// </summary>
public const string PlaceType = "placeType";
/// <summary>
/// The previous page parameter.
/// </summary>
public const string PreviousPage = "previousPage";
/// <summary>
/// The provider identifier parameter.
/// </summary>
public const string ProviderId = "providerId";
/// <summary>
/// The provider type identifier parameter.
/// </summary>
public const string ProviderTypeId = "providerTypeId";
/// <summary>
/// The request identifier parameter.
/// </summary>
public const string RequestId = "requestId";
/// <summary>
/// The RNT identifier parameter.
/// </summary>
public const string RntId = "rntId";
/// <summary>
/// The route parameter.
/// </summary>
public const string Route = "route";
/// <summary>
/// The operation parameter.
/// </summary>
public const string Operation = "operation";
/// <summary>
/// The other ticket price parameter.
/// </summary>
public const string OtherTicketPrice = "otherTicketPrice";
/// <summary>
/// The path parameter.
/// </summary>
public const string Path = "path";
/// <summary>
/// The saturday parameter.
/// </summary>
public const string Saturday = "saturday";
/// <summary>
/// The secondary parameter.
/// </summary>
public const string Secondary = "secondary";
/// <summary>
/// The service health parameter.
/// </summary>
public const string ServiceHealth = "serviceHealth";
/// <summary>
/// The service identifier parameter.
/// </summary>
public const string ServiceId = "serviceId";
/// <summary>
/// The severity parameter.
/// </summary>
public const string Severity = "severity";
/// <summary>
/// The state identifier parameter.
/// </summary>
public const string StateId = "stateId";
/// <summary>
/// The region identifier parameter.
/// </summary>
public const string StateProvinceRegionId = "regionId";
/// <summary>
/// The sunday parameter.
/// </summary>
public const string Sunday = "sunday";
/// <summary>
/// The tax identifier parameter.
/// </summary>
public const string TaxId = "taxId";
/// <summary>
/// The target parameter.
/// </summary>
public const string Target = "target";
/// <summary>
/// The thursday parameter.
/// </summary>
public const string Thursday = "thursday";
/// <summary>
/// The timestamp parameter.
/// </summary>
public const string Timestamp = "timestamp";
/// <summary>
/// The total items parameter.
/// </summary>
public const string TotalItems = "totalItems";
/// <summary>
/// Gets or sets the transaction identifier parameter.
/// </summary>
public const string TransactionId = "transactionId";
/// <summary>
/// The tuesday parameter.
/// </summary>
public const string Tuesday = "tuesday";
/// <summary>
/// The URI parameter.
/// </summary>
public const string Uri = "uri";
/// <summary>
/// The update parameter.
/// </summary>
public const string Update = "update";
/// <summary>
/// The x-forwarded-for header parameter.
/// </summary>
public const string XForwardedFor = "xForwardedFor";
/// <summary>
/// The x-forwarded-for header parameter.
/// </summary>
public const string XForwardedForHeader = "X-Forwarded-For";
/// <summary>
/// The final currency identifier parameter.
/// </summary>
public const string CurrencyId = "currencyId";
/// <summary>
/// The user identifier parameter.
/// </summary>
public const string UserId = "userId";
/// <summary>
/// The user parameter.
/// </summary>
public const string User = "user";
/// <summary>
/// The warning log severity level.
/// </summary>
public const string Warning = "warning";
/// <summary>
/// The email parameter.
/// </summary>
public const string Email = "email";
}
}

View File

@ -0,0 +1,21 @@
// ***********************************************************************
// <copyright file="EnvironmentVariables.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// Constants of the environment variables for this service.
/// </summary>
public static class EnvironmentVariables
{
/// <summary>
/// The stage environment vriable.
/// </summary>
public const string Stage = "ASPNETCORE_ENVIRONMENT";
}
}

View File

@ -0,0 +1,79 @@
// ***********************************************************************
// <copyright file="ErrorCodes.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// Constants for the error codes.
/// </summary>
public static class ErrorCodes
{
/// <summary>
/// The generic entities not found error code.
/// </summary>
public const string EntitiesNotFound = "{0}EntitiesNotFound";
/// <summary>
/// The entity already exsits error message.
/// </summary>
public const string EntityAlreadyExists = "{0}EntityAlreadyExists";
/// <summary>
/// The generic entity not found error code.
/// </summary>
public const string EntityNotFound = "{0}EntityNotFound";
/// <summary>
/// The generic not supported error code.
/// </summary>
public const string EntityNotSupported = "{0}NotSupported";
/// <summary>
/// The internal server error code.
/// </summary>
public const string InternalServerError = "InternalServerError";
/// <summary>
/// The invalid parameters in mapper error code.
/// </summary>
public const string InvalidParametersMapper = "InvalidParametersMapper";
/// <summary>
/// The page size invalid value error code.
/// </summary>
public const string PageSizeInvalidValue = "PageSizeInvalidValue";
/// <summary>
/// The page ot of range error code.
/// </summary>
public const string PageOutOfRange = "PageOutOfRange";
/// <summary>
/// The property does not match the regular expresion error code.
/// </summary>
public const string PropertyDoesNotMatchRegex = "{0}PropertyDoesNotMatchRegex";
/// <summary>
/// The property is required error code.
/// </summary>
public const string PropertyIsRequired = "{0}PropertyIsRequired";
/// <summary>
/// The property length invalid error code.
/// </summary>
public const string PropertyLengthInvalid = "{0}PropertyLengthInvalid";
/// <summary>
/// The property must be in range error code.
/// </summary>
public const string PropertyMustBeInRange = "{0}PropertyMustBeInRange";
/// <summary>
/// The route not found error code.
/// </summary>
public const string RouteNotFound = "RouteNotFound";
}
}

View File

@ -0,0 +1,10 @@
namespace Core.Blueprint.Logging
{
public static class Headers
{
/// <summary>
/// The authorization header.
/// </summary>
public const string Authorization = "Authorization";
}
}

View File

@ -0,0 +1,148 @@
// ***********************************************************************
// <copyright file="MimeTypes.cs">
// Heath
// </copyright>
// ***********************************************************************
using System.Globalization;
namespace Core.Blueprint.Logging
{
/// <summary>
/// Constants for the mime types.
/// </summary>
public static class MimeTypes
{
/// <summary>
/// The service application/json mime type.
/// </summary>
public const string ApplicationJson = "application/json";
/// <summary>
/// The application/pdf mime type.
/// </summary>
public const string ApplicationPdf = "application/pdf";
/// <summary>
/// The end index.
/// </summary>
public const int EndIndex = 5;
/// <summary>
/// The JPEG extension.
/// </summary>
public const string ExtensionGif = "gif";
/// <summary>
/// The JPEG extension.
/// </summary>
public const string ExtensionJpeg = "jpeg";
/// <summary>
/// The PNG extension.
/// </summary>
public const string ExtensionPng = "png";
/// <summary>
/// The SVG extension.
/// </summary>
public const string ExtensionSvg = "svg";
/// <summary>
/// The image/gif mime type.
/// </summary>
public const string ImageGif = "image/gif";
/// <summary>
/// The image/jpeg mime type.
/// </summary>
public const string ImageJpeg = "image/jpeg";
/// <summary>
/// The image/png mime type.
/// </summary>
public const string ImagePng = "image/png";
/// <summary>
/// The image/svg+xml mime type.
/// </summary>
public const string ImageSvg = "image/svg+xml";
/// <summary>
/// The identifier GIF.
/// </summary>
public const string IdentifierGif = "R0LGO";
/// <summary>
/// The identifier PNG.
/// </summary>
public const string IdentifierJpeg = "/9J/4";
/// <summary>
/// The identifier PDF.
/// </summary>
public const string IdentifierPdf = "JVBER";
/// <summary>
/// The identifier PNG.
/// </summary>
public const string IdentifierPng = "IVBOR";
/// <summary>
/// The identifier SVG.
/// </summary>
public const string IdentifierSvg = "PHN2Z";
/// <summary>
/// The parameter name.
/// </summary>
public const string ParameterName = "MimeType";
/// <summary>
/// The start index.
/// </summary>
public const int StartIndex = 0;
/// <summary>
/// The mime type dictionary.
/// </summary>
public static readonly Dictionary<string, string> Dictionary = new Dictionary<string, string>
{
{ IdentifierJpeg, ImageJpeg },
{ IdentifierPng, ImagePng },
{ IdentifierGif, ImageGif },
{ IdentifierSvg, ImageSvg },
};
/// <summary>
/// The mime type dictionary.
/// </summary>
public static readonly Dictionary<string, string> DictionaryExtension = new Dictionary<string, string>
{
{ IdentifierJpeg, ExtensionJpeg },
{ IdentifierPng, ExtensionPng },
{ IdentifierGif, ExtensionGif },
{ IdentifierSvg, ExtensionSvg },
};
/// <summary>
/// Gets the mime type.
/// </summary>
/// <param name="content">The cpntent with mime type identifier, substring 0, 5 from content.</param>
/// <returns>A <see cref="string"/> representing the value.</returns>
public static string GetMimeType(this string content)
{
return Dictionary.FirstOrDefault(_ => _.Key == content[..EndIndex].ToUpper(CultureInfo.InvariantCulture)).Value;
}
/// <summary>
/// Gets the extension.
/// </summary>
/// <param name="content">The mime type identifier, substring 0, 5 from content.</param>
/// <returns>A <see cref="string"/> representing the value.</returns>
public static string GetExtension(this string content)
{
return DictionaryExtension.FirstOrDefault(_ => _.Key == content[..EndIndex].ToUpper(CultureInfo.InvariantCulture)).Value;
}
}
}

View File

@ -0,0 +1,29 @@
// ***********************************************************************
// <copyright file="Responses.cs">
// Heath
// </copyright>
// ***********************************************************************
namespace Core.Blueprint.Logging
{
/// <summary>
/// Constants of the responses for this service.
/// </summary>
public static class Responses
{
/// <summary>
/// The health response.
/// </summary>
public const string HealthyService = "healthy";
/// <summary>
/// The route does not exist response.
/// </summary>
public const string RouteDoesNotExist = "The specified route '{0}' does not exist for method '{1}' in this service.";
/// <summary>
/// The target response.
/// </summary>
public const string Target = "{0}|{1}://{2}{3}";
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,92 @@
// ***********************************************************************
// <copyright file="HttpErrorMiddleware.cs">
// Heath
// </copyright>
// ***********************************************************************
using Microsoft.AspNetCore.Http;
using Serilog;
using System.Text.Json;
namespace Core.Blueprint.Logging
{
/// <summary>
/// Handles HTTP logging.
/// </summary>
public class HttpErrorMiddleware
{
private readonly ILogger logger;
private readonly RequestDelegate requestProcess;
public readonly ServiceSettings settings;
/// <summary>
/// Creates a new instrance of <see cref="HttpErrorMiddleware"/>.
/// </summary>
/// <param name="logger">The logger representig an instance of <see cref="ILogger"/>.</param>
/// <param name="requestProcess">The request delegate process.</param>
public HttpErrorMiddleware(ILogger logger, RequestDelegate requestProcess, ServiceSettings settings)
{
this.logger = logger;
this.requestProcess = requestProcess;
this.settings = settings;
}
/// <summary>
/// Invoke method.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
}
}
/// <summary>
/// Handles error responses.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <param name="message">The error message.</param>
/// <param name="errorCode">The error code.</param>
/// <param name="statusCode">The HTTP status code.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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<HttpError>(context, errorMessage, $"{settings.ApplicationName}-{settings.LayerName}");
context.Response.ContentType = MimeTypes.ApplicationJson;
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(JsonSerializer.Serialize(errorMessage)).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,237 @@
// ***********************************************************************
// <copyright file="HttpLogger.cs">
// Heath
// </copyright>
// ***********************************************************************
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
{
/// <summary>
/// Handles all logging scenarios.
/// </summary>
public static class HttpLogger
{
/// <summary>
/// The JSON serializer options for logging methods.
/// </summary>
public static JsonSerializerOptions serializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = {
new JsonStringEnumConverter( JsonNamingPolicy.CamelCase),
},
};
/// <summary>
/// Logs an error message.
/// </summary>
/// <typeparam name="TMessage">The generic message parameter.</typeparam>
/// <param name="context">The HTTP context.</param>
/// <param name="message">The message.</param>
/// <param name="serviceId">The service identifier.</param>
public static void LogError<TMessage>(this ILogger logger, HttpContext context, TMessage message, string? serviceId)
{
var logMessage = CreateErrorLog(context, message, serviceId);
logger.Error(logMessage);
}
/// <summary>
/// Logs an information message.
/// </summary>
/// <typeparam name="TMessage">The generic message parameter.</typeparam>
/// <param name="context">The HTTP context.</param>
/// <param name="message">The message.</param>
/// <param name="serviceId">The service identifier.</param>
public static void LogInfo<TMessage>(this ILogger logger, HttpContext context, TMessage message, string? serviceId)
{
var logMessage = CreateInfoLog(context, message, serviceId);
logger.Information(logMessage);
}
/// <summary>
/// Logs an incoming HTTP request.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="context">The HTTP context.</param>
/// <param name="recyclableMemoryStreamManager">The recyclable mmory stream manager.</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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;
}
/// <summary>
/// Logs an outcome HTTP response.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="context">The HTTP context.</param>
/// <param name="recyclableMemoryStreamManager">The recyclable mmory stream manager.</param>
/// <param name="requestProcess">The request delegate process.</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
///
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);
}
/// <summary>
/// Creates an error log.
/// </summary>
/// <typeparam name="TMessage">The generic message.</typeparam>
/// <param name="context">The HTTP context.</param>
/// <param name="message">The error message.</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="string"/> representig the error log.</returns>
private static string CreateErrorLog<TMessage>(HttpContext context, TMessage message, string? serviceId)
=> CreateLog(context, LogSeverity.Error, LogOperation.Error, message, serviceId);
/// <summary>
/// Creates an info log.
/// </summary>
/// <typeparam name="TMessage">The generic message.</typeparam>
/// <param name="context">The HTTP context.</param>
/// <param name="message">The info message.</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="string"/> representig the info log.</returns>
private static string CreateInfoLog<TMessage>(HttpContext context, TMessage message, string? serviceId)
=> CreateLog(context, LogSeverity.Info, LogOperation.Info, message, serviceId);
/// <summary>
/// Creates a request log.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <param name="requestBody">The request body.</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="string"/> representig the request log.</returns>
private static string CreateRequestLog(HttpContext context, string? requestBody, string? serviceId)
=> CreateLog(context, LogSeverity.Info, LogOperation.ClientRequest, requestBody, serviceId);
/// <summary>
/// Creates a response log.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <param name="responseBody">The response body.</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="string"/> representig the response log.</returns>
private static string CreateResponseLog(HttpContext context, string? responseBody, string? serviceId)
=> CreateLog(context, LogSeverity.Info, LogOperation.ClientResponse, responseBody, serviceId);
/// <summary>
/// Creates a generic log.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <param name="severity">The log severity.</param>
/// <param name="operation">The log operation.</param>
/// <param name="message">The log message</param>
/// <param name="serviceId">The service identifier.</param>
/// <returns>A <see cref="string"/> representing a generic log.</returns>
private static string CreateLog<TMessage>(
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<TMessage>
{
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", "");
}
/// <summary>
/// Reads the stream.
/// </summary>
/// <param name="stream">The stream to be read.</param>
/// <returns>A <see cref="string?"/> representig the request body.</returns>
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;
}
}
}

View File

@ -0,0 +1,69 @@
// ***********************************************************************
// <copyright file="HttpLoggingMiddleware.cs">
// Heath
// </copyright>
// ***********************************************************************
using Microsoft.AspNetCore.Http;
using Microsoft.IO;
using Serilog;
namespace Core.Blueprint.Logging
{
/// <summary>
/// Handles HTTP logging.
/// </summary>
public class HttpLoggingMiddleware
{
private readonly ILogger logger;
private readonly RequestDelegate requestProcess;
private readonly ServiceSettings settings;
private readonly RecyclableMemoryStreamManager recyclableMemoryStreamManager;
/// <summary>
/// Creates a new instrance of <see cref="HttpLoggingMiddleware"/>.
/// </summary>
/// <param name="requestProcess">The request delegate process.</param>
/// <param name="logger">The logger representig an instance of <see cref="ILogger"/>.</param>
/// <param name="settings">The service settings.</param>
public HttpLoggingMiddleware(RequestDelegate requestProcess, ILogger logger, ServiceSettings settings)
{
this.logger = logger;
this.requestProcess = requestProcess;
this.settings = settings;
recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
}
/// <summary>
/// Invoke method.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
await LogRequest(context);
await LogResponse(context);
}
/// <summary>
/// Logs an incoming HTTP request.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task LogRequest(HttpContext context)
{
await logger.LogRequest(context, recyclableMemoryStreamManager, $"{settings.ApplicationName}-{settings.LayerName}");
}
/// <summary>
/// Logs an outcome HTTP response.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task LogResponse(HttpContext context)
{
await logger.LogResponse(context, recyclableMemoryStreamManager, requestProcess, $"{settings.ApplicationName}-{settings.LayerName}");
}
}
}

View File

@ -0,0 +1,89 @@
namespace Core.Blueprint.Logging
{
/// <summary>
/// Provides logging functionalities using Serilog.
/// </summary>
public class LoggerProvider : ILoggerProvider
{
private readonly Serilog.ILogger logger;
/// <summary>
/// Initializes a new instance of the <see cref="LoggerProvider"/> class.
/// </summary>
/// <param name="logger">The Serilog logger instance.</param>
public LoggerProvider(Serilog.ILogger logger)
{
this.logger = logger;
}
/// <summary>
/// Logs an informational message for a specific service.
/// </summary>
/// <param name="service">The name of the service.</param>
/// <param name="args">Additional arguments to include in the log.</param>
public void LogInformation(string service, params object[] args)
{
logger.Information("Starting operation in {service} service", service, args);
}
/// <summary>
/// Logs a message indicating the start of an operation in a specific service.
/// </summary>
/// <param name="service">The name of the service.</param>
/// <param name="args">Additional parameters associated with the operation.</param>
public void LogOperationStarted(string service, params object[] args)
{
logger.Information("Starting operation in {Service} service with parameters: {@Args}", service, args);
}
/// <summary>
/// Logs a message indicating the completion of an operation in a specific service.
/// </summary>
/// <param name="service">The name of the service.</param>
/// <param name="args">Additional parameters associated with the operation.</param>
public void LogOperationFinished(string service, params object[] args)
{
logger.Information("Finishing operation in {Service} service with parameters: {@Args}", service, args);
}
/// <summary>
/// Logs a general informational message.
/// </summary>
/// <param name="message">The message to log.</param>
public void LogInformation(string message)
{
logger.Information(message);
}
/// <summary>
/// Logs a warning message with additional context.
/// </summary>
/// <param name="message">The warning message to log.</param>
/// <param name="args">Additional arguments to include in the log.</param>
public void LogWarning(string message, params object[] args)
{
logger.Warning(message, args);
}
/// <summary>
/// Logs an error that occurred in a specific service.
/// </summary>
/// <param name="service">The name of the service.</param>
/// <param name="args">Additional details about the error.</param>
public void LogError(string service, params object[] args)
{
logger.Error("An error occurred in `{service}` Exception: {@Args}", service, args);
}
/// <summary>
/// Logs a critical error with an exception, message, and additional context.
/// </summary>
/// <param name="exception">The exception associated with the critical error.</param>
/// <param name="message">The critical error message.</param>
/// <param name="args">Additional arguments to include in the log.</param>
public void LogCritical(Exception exception, string message, params object[] args)
{
logger.Fatal(exception, message, args);
}
}
}

View File

@ -0,0 +1,24 @@
namespace Core.Blueprint.Mongo
{
/// <summary>
/// The <see cref="CollectionAttributeName"/> 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.
/// </summary>
[AttributeUsage(AttributeTargets.Class)] // This attribute can only be applied to classes.
public class CollectionAttributeName : Attribute
{
/// <summary>
/// Gets or sets the name of the MongoDB collection that the class is mapped to.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="CollectionAttributeName"/> class with the specified collection name.
/// </summary>
/// <param name="name">The name of the MongoDB collection that the class should be mapped to.</param>
public CollectionAttributeName(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name), "Collection name cannot be null.");
}
}
}

View File

@ -0,0 +1,121 @@
using Azure.Core;
using Azure.Identity;
using MongoDB.Driver.Authentication.Oidc;
namespace Core.Blueprint.Mongo.Configuration
{
/// <summary>
/// The <see cref="HeathIdentityProvider"/> class is responsible for acquiring an OpenID Connect (OIDC)
/// access token for MongoDB authentication using Azure Identity and Managed Identity credentials.
/// </summary>
public class HeathIdentityProvider : IOidcCallback
{
/// <summary>
/// The audience (resource identifier) for which the OIDC token is being requested.
/// </summary>
private readonly string _audience;
/// <summary>
/// The environment in which the application is running (e.g., Development, Production).
/// </summary>
private readonly string _environment;
/// <summary>
/// Initializes a new instance of the <see cref="HeathIdentityProvider"/> class with the specified audience.
/// </summary>
/// <param name="audience">The audience (resource identifier) for which the OIDC token is being requested.</param>
public HeathIdentityProvider(string audience)
{
_audience = audience;
_environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty;
}
/// <summary>
/// Synchronously retrieves the OIDC access token to authenticate to MongoDB.
/// </summary>
/// <param name="parameters">The callback parameters provided for the OIDC request.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An OIDC access token to authenticate to MongoDB.</returns>
/// <exception cref="Exception">Thrown if an error occurs during the token acquisition process.</exception>
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}");
}
}
/// <summary>
/// Asynchronously retrieves the OIDC access token to authenticate to MongoDB.
/// </summary>
/// <param name="parameters">The callback parameters provided for the OIDC request.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation, with an OIDC access token as the result.</returns>
/// <exception cref="Exception">Thrown if an error occurs during the token acquisition process.</exception>
public async Task<OidcAccessToken> 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}");
}
}
}
}

View File

@ -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
{
/// <summary>
/// The <see cref="RegisterBlueprint"/> class contains extension methods for registering the MongoDB context and configuration settings
/// to the <see cref="IServiceCollection"/> in the dependency injection container.
/// </summary>
public static class RegisterBlueprint
{
/// <summary>
/// Adds the MongoDB layer services to the <see cref="IServiceCollection"/>.
/// Registers the MongoDB context and configuration settings for MongoDB connection, database name, and audience.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to which the services will be added.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> used to load MongoDB settings.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with MongoDB services registered.</returns>
public static IServiceCollection AddMongoLayer(this IServiceCollection services, IConfiguration configuration)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty;
services.AddSingleton<IMongoContext, MongoContext>();
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<MongoDbSettings>(options =>
{
options.ConnectionString = ConnectionString;
options.Databasename = Databasename;
options.Audience = Audience;
});
services.AddSingleton<IMongoClient>(serviceProvider =>
{
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value;
var mongoClientSettings = MongoClientSettings.FromConnectionString(settings.ConnectionString);
mongoClientSettings.Credential = MongoCredential.CreateOidcCredential(new HeathIdentityProvider(settings.Audience));
return new MongoClient(mongoClientSettings);
});
services.AddSingleton<IMongoDatabase>(serviceProvider =>
{
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value;
var client = serviceProvider.GetRequiredService<IMongoClient>();
return client.GetDatabase(settings.Databasename);
});
services.AddSingleton<IMongoDbSettings>(serviceProvider => serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value);
return services;
}
}
}

View File

@ -0,0 +1,66 @@
using Microsoft.Extensions.Configuration;
namespace Core.Blueprint.Mongo
{
/// <summary>
/// The <see cref="MongoContext"/> class represents the MongoDB context that contains the connection information,
/// including the connection string, database name, and audience.
/// It implements the <see cref="IMongoContext"/> interface to provide methods for accessing these values.
/// </summary>
public sealed class MongoContext : IMongoContext
{
/// <summary>
/// Gets or sets the connection string used to connect to the MongoDB instance.
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the name of the MongoDB database.
/// </summary>
public string Databasename { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the audience (resource identifier) used for MongoDB authentication.
/// </summary>
public string Audience { get; set; } = string.Empty;
private readonly IConfiguration configuration;
/// <summary>
/// Initializes a new instance of the <see cref="MongoContext"/> class using the provided <see cref="IConfiguration"/>.
/// The configuration is used to retrieve MongoDB connection settings.
/// </summary>
/// <param name="configuration">The configuration used to retrieve the MongoDB connection settings.</param>
public MongoContext(IConfiguration configuration)
{
this.configuration = configuration;
}
/// <summary>
/// Retrieves the MongoDB connection string from the configuration.
/// </summary>
/// <returns>The MongoDB connection string, or an empty string if not found.</returns>
public string GetConnectionString()
{
return configuration.GetConnectionString("MongoDb:ConnectionString")?.ToString() ?? string.Empty;
}
/// <summary>
/// Retrieves the MongoDB database name from the configuration.
/// </summary>
/// <returns>The MongoDB database name, or an empty string if not found.</returns>
public string GetDatabasename()
{
return configuration.GetSection("MongoDb:DatabaseName").Value ?? string.Empty;
}
/// <summary>
/// Retrieves the MongoDB audience (resource identifier) from the configuration.
/// </summary>
/// <returns>The MongoDB audience, or an empty string if not found.</returns>
public string GetAudience()
{
return configuration.GetSection("MongoDb:Audience").Value ?? string.Empty;
}
}
}

View File

@ -0,0 +1,25 @@
namespace Core.Blueprint.Mongo
{
/// <summary>
/// Represents the MongoDB configuration settings, including the connection string,
/// database name, and audience, used for connecting and authenticating to a MongoDB instance.
/// Implements the <see cref="IMongoDbSettings"/> interface to provide a strongly typed configuration.
/// </summary>
public class MongoDbSettings : IMongoDbSettings
{
/// <summary>
/// Gets or sets the connection string used to connect to the MongoDB instance.
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the name of the MongoDB database to connect to.
/// </summary>
public string Databasename { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the audience (resource identifier) used for MongoDB authentication.
/// </summary>
public string Audience { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,152 @@
using MongoDB.Driver;
using System.Linq.Expressions;
namespace Core.Blueprint.Mongo
{
/// <summary>
/// Interface for performing CRUD operations and queries on MongoDB collections.
/// The <typeparamref name="TDocument"/> represents the type of documents in the collection,
/// which must implement the <see cref="IDocument"/> interface.
/// </summary>
/// <typeparam name="TDocument">The type of document in the MongoDB collection, must implement <see cref="IDocument"/>.</typeparam>
public interface ICollectionsRepository<TDocument> where TDocument : IDocument
{
/// <summary>
/// Retrieves all documents from the collection as an enumerable queryable result.
/// </summary>
/// <returns>A <see cref="ValueTask"/> containing an <see cref="IEnumerable{TDocument}"/> representing the collection's documents.</returns>
ValueTask<IEnumerable<TDocument>> AsQueryable();
/// <summary>
/// Filters the documents in the collection by the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents based on the provided condition.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation, with a result of an <see cref="IEnumerable{TDocument}"/> of filtered documents.</returns>
Task<IEnumerable<TDocument>> FilterBy(
Expression<Func<TDocument, bool>> filterExpression);
/// <summary>
/// Filters the documents in the collection by the provided filter expression and projects them to a different type.
/// </summary>
/// <typeparam name="TProjected">The type to project the documents into.</typeparam>
/// <param name="filterExpression">An expression used to filter the documents.</param>
/// <param name="projectionExpression">An expression used to project the filtered documents into the <typeparamref name="TProjected"/> type.</param>
/// <returns>An <see cref="IEnumerable{TProjected}"/> representing the projected documents.</returns>
IEnumerable<TProjected> FilterBy<TProjected>(
Expression<Func<TDocument, bool>> filterExpression,
Expression<Func<TDocument, TProjected>> projectionExpression);
/// <summary>
/// Filters documents in the collection based on the provided MongoDB filter definition.
/// </summary>
/// <param name="filterDefinition">A filter definition for MongoDB query.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of documents that match the filter.</returns>
Task<IEnumerable<TDocument>> FilterByMongoFilterAsync(FilterDefinition<TDocument> filterDefinition);
/// <summary>
/// Finds a single document by the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents.</param>
/// <returns>The first matching <see cref="TDocument"/> or null if no match is found.</returns>
TDocument FindOne(Expression<Func<TDocument, bool>> filterExpression);
/// <summary>
/// Asynchronously finds a single document by the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation, with the matching <see cref="TDocument"/> or null.</returns>
Task<TDocument> FindOneAsync(Expression<Func<TDocument, bool>> filterExpression);
/// <summary>
/// Finds a document by its identifier.
/// </summary>
/// <param name="id">The identifier of the document.</param>
/// <returns>The document with the provided identifier or null if not found.</returns>
TDocument FindById(string id);
/// <summary>
/// Asynchronously finds a document by its identifier.
/// </summary>
/// <param name="id">The identifier of the document.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation, with the matching <see cref="TDocument"/> or null.</returns>
Task<TDocument> FindByIdAsync(string id);
/// <summary>
/// Inserts a single document into the collection.
/// </summary>
/// <param name="document">The document to insert.</param>
void InsertOne(TDocument document);
/// <summary>
/// Asynchronously inserts a single document into the collection.
/// </summary>
/// <param name="document">The document to insert.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task InsertOneAsync(TDocument document);
/// <summary>
/// Inserts multiple documents into the collection.
/// </summary>
/// <param name="documents">The collection of documents to insert.</param>
void InsertMany(ICollection<TDocument> documents);
/// <summary>
/// Asynchronously inserts multiple documents into the collection.
/// </summary>
/// <param name="documents">The collection of documents to insert.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task InsertManyAsync(ICollection<TDocument> documents);
/// <summary>
/// Replaces an existing document with a new one.
/// </summary>
/// <param name="document">The document to replace the existing one.</param>
void ReplaceOne(TDocument document);
/// <summary>
/// Asynchronously replaces an existing document with a new one.
/// </summary>
/// <param name="document">The document to replace the existing one.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task ReplaceOneAsync(TDocument document);
/// <summary>
/// Deletes a single document by the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents to delete.</param>
void DeleteOne(Expression<Func<TDocument, bool>> filterExpression);
/// <summary>
/// Asynchronously deletes a single document by the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents to delete.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<TDocument> DeleteOneAsync(Expression<Func<TDocument, bool>> filterExpression);
/// <summary>
/// Deletes a single document by its identifier.
/// </summary>
/// <param name="id">The identifier of the document to delete.</param>
void DeleteById(string id);
/// <summary>
/// Asynchronously deletes a single document by its identifier.
/// </summary>
/// <param name="id">The identifier of the document to delete.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteByIdAsync(string id);
/// <summary>
/// Deletes multiple documents that match the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents to delete.</param>
void DeleteMany(Expression<Func<TDocument, bool>> filterExpression);
/// <summary>
/// Asynchronously deletes multiple documents that match the provided filter expression.
/// </summary>
/// <param name="filterExpression">An expression used to filter the documents to delete.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteManyAsync(Expression<Func<TDocument, bool>> filterExpression);
}
}

View File

@ -0,0 +1,39 @@
using Core.Blueprint.Mongo;
public interface IDocument
{
/// <summary>
/// Gets or sets the MongoDB ObjectId for the document.
/// </summary>
string _Id { get; }
/// <summary>
/// Gets or sets a unique identifier for the document, represented as a string (GUID).
/// </summary>
string Id { get; }
/// <summary>
/// Gets or sets the timestamp of when the document was created.
/// </summary>
DateTime CreatedAt { get; }
/// <summary>
/// Gets or sets the user or system who created the document.
/// </summary>
string? CreatedBy { get; set; }
/// <summary>
/// Gets or sets the timestamp of when the document was last updated.
/// </summary>
DateTime? UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the user or system who last updated the document.
/// </summary>
string? UpdatedBy { get; set; }
/// <summary>
/// Gets or sets the status of the document.
/// </summary>
StatusEnum? Status { get; set; }
}

View File

@ -0,0 +1,45 @@
namespace Core.Blueprint.Mongo
{
/// <summary>
/// Represents the context for interacting with MongoDB, providing access to connection-related information,
/// such as the connection string, database name, and audience for authentication.
/// </summary>
public interface IMongoContext
{
/// <summary>
/// Gets the connection string used to connect to the MongoDB instance.
/// </summary>
/// <returns>A string representing the MongoDB connection string.</returns>
string GetConnectionString();
/// <summary>
/// Gets the name of the MongoDB database.
/// </summary>
/// <returns>A string representing the MongoDB database name.</returns>
string GetDatabasename();
/// <summary>
/// Gets the audience (resource identifier) used for MongoDB authentication.
/// </summary>
/// <returns>A string representing the MongoDB audience (typically the resource identifier for authentication).</returns>
string GetAudience();
/// <summary>
/// Gets or sets the MongoDB connection string used to connect to the database.
/// </summary>
/// <value>A string representing the MongoDB connection string.</value>
string ConnectionString { get; set; }
/// <summary>
/// Gets or sets the name of the MongoDB database.
/// </summary>
/// <value>A string representing the MongoDB database name.</value>
string Databasename { get; set; }
/// <summary>
/// Gets or sets the audience (resource identifier) for MongoDB authentication.
/// </summary>
/// <value>A string representing the MongoDB audience (resource identifier for authentication).</value>
string Audience { get; set; }
}
}

View File

@ -0,0 +1,32 @@
namespace Core.Blueprint.Mongo
{
/// <summary>
/// Represents the settings required to connect to a MongoDB instance, including the connection string,
/// database name, and audience used for authentication.
/// </summary>
public interface IMongoDbSettings
{
/// <summary>
/// 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.
/// </summary>
/// <value>A string representing the MongoDB connection string.</value>
string ConnectionString { get; set; }
/// <summary>
/// Gets or sets the name of the MongoDB database to connect to.
/// This value specifies which database to use within the MongoDB instance.
/// </summary>
/// <value>A string representing the name of the MongoDB database.</value>
string Databasename { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <value>A string representing the MongoDB audience (resource identifier for authentication).</value>
string Audience { get; set; }
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.44.1" />
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.0" />
<PackageReference Include="MongoDB.Bson" Version="3.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
</ItemGroup>
</Project>

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
[BsonId]
[BsonElement("_id")]
[BsonRepresentation(BsonType.ObjectId)]
[JsonPropertyName("_id")]
public string _Id { get; init; }
/// <summary>
/// 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.
/// </summary>
[BsonElement("id")]
[BsonRepresentation(BsonType.String)]
[JsonPropertyName("id")]
public string Id { get; init; }
/// <summary>
/// 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.
/// </summary>
[BsonElement("createdAt")]
[BsonRepresentation(BsonType.DateTime)]
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; init; }
/// <summary>
/// Gets or sets the user or system who created the document.
/// This field can be used for audit purposes.
/// </summary>
[BsonElement("createdBy")]
[BsonRepresentation(BsonType.String)]
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
/// <summary>
/// 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.
/// </summary>
[BsonElement("updatedAt")]
[BsonRepresentation(BsonType.DateTime)]
[JsonPropertyName("updatedAt")]
public DateTime? UpdatedAt { get; set; } = null;
/// <summary>
/// Gets or sets the user or system who last updated the document.
/// This field can be used for audit purposes.
/// </summary>
[BsonElement("updatedBy")]
[BsonRepresentation(BsonType.String)]
[JsonPropertyName("updatedBy")]
public string? UpdatedBy { get; set; } = null;
/// <summary>
/// Gets or sets the status of the document.
/// The status is represented by an enum and defaults to <see cref="StatusEnum.Active"/>.
/// </summary>
[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;
}
}

View File

@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Core.Blueprint.Mongo
{
/// <summary>
/// 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.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum StatusEnum
{
/// <summary>
/// Represents an active document or entity.
/// </summary>
Active = 0,
/// <summary>
/// Represents an inactive document or entity.
/// </summary>
Inactive = 1,
/// <summary>
/// Represents a deleted document or entity.
/// </summary>
Deleted = 2
}
}

View File

@ -0,0 +1,40 @@
using MongoDB.Driver;
namespace Core.Blueprint.Mongo
{
/// <summary>
/// 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.
/// </summary>
public class MongoProvider
{
private readonly IMongoDatabase _database;
/// <summary>
/// Initializes a new instance of the <see cref="MongoProvider"/> 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.
/// </summary>
/// <param name="mongoDbSettings">The MongoDB settings required for connecting to the database.</param>
public MongoProvider(IMongoDatabase database)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
}
/// <summary>
/// Gets the initialized MongoDB database. If the database is not initialized, an exception is thrown.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the database connection is not initialized.</exception>
protected IMongoDatabase Database
{
get
{
if (_database == null)
{
throw new InvalidOperationException("MongoDB connection is not initialized.");
}
return _database;
}
}
}
}

View File

@ -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
{
/// <summary>
/// Provides methods for interacting with a MongoDB collection for a specific document type.
/// Inherits from <see cref="MongoProvider"/> and implements <see cref="ICollectionsRepository{TDocument}"/>.
/// This class encapsulates common database operations such as querying, inserting, updating, and deleting documents.
/// </summary>
/// <typeparam name="TDocument">The type of document stored in the collection, which must implement <see cref="IDocument"/>.</typeparam>
public class CollectionRepository<TDocument>(IMongoDatabase database) : ICollectionsRepository<TDocument> where TDocument : IDocument
{
private IMongoCollection<TDocument> _collection;
/// <summary>
/// Initializes the MongoDB collection based on the <see cref="CollectionAttributeName"/> attribute
/// applied to the <typeparamref name="TDocument"/> type. Throws an exception if the attribute is not present.
/// </summary>
public void CollectionInitialization()
{
var collectionAttribute = typeof(TDocument).GetCustomAttribute<CollectionAttributeName>();
if (collectionAttribute == null)
{
throw new InvalidOperationException($"The class {typeof(TDocument).Name} is missing the CollectionAttributeName attribute.");
}
string collectionName = collectionAttribute.Name;
_collection = database.GetCollection<TDocument>(collectionName);
}
/// <summary>
/// Returns all documents from the collection as an enumerable list.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable list of documents.</returns>
public virtual async ValueTask<IEnumerable<TDocument>> AsQueryable()
{
return await _collection.AsQueryable().ToListAsync();
}
/// <summary>
/// Filters documents in the collection based on the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the documents.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of documents that match the filter.</returns>
public virtual async Task<IEnumerable<TDocument>> FilterBy(
Expression<Func<TDocument, bool>> filterExpression)
{
var objectResult = await _collection.FindAsync(filterExpression).ConfigureAwait(false);
return objectResult.ToList();
}
/// <summary>
/// Filters documents in the collection based on the provided filter and projection expressions.
/// Projects the filtered documents into a new type.
/// </summary>
/// <typeparam name="TProjected">The type to project the documents into.</typeparam>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the documents.</param>
/// <param name="projectionExpression">A lambda expression that defines how the documents should be projected.</param>
/// <returns>An enumerable collection of projected documents.</returns>
public virtual IEnumerable<TProjected> FilterBy<TProjected>(
Expression<Func<TDocument, bool>> filterExpression,
Expression<Func<TDocument, TProjected>> projectionExpression)
{
return _collection.Find(filterExpression).Project(projectionExpression).ToEnumerable();
}
/// <summary>
/// Finds a single document that matches the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the document.</param>
/// <returns>The document that matches the filter, or <c>null</c> if no document is found.</returns>
public virtual TDocument FindOne(Expression<Func<TDocument, bool>> filterExpression)
{
return _collection.Find(filterExpression).FirstOrDefault();
}
/// <summary>
/// Filters documents in the collection based on the provided MongoDB filter definition.
/// </summary>
/// <param name="filterDefinition">A filter definition for MongoDB query.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of documents that match the filter.</returns>
public virtual async Task<IEnumerable<TDocument>> FilterByMongoFilterAsync(FilterDefinition<TDocument> filterDefinition)
{
var objectResult = await _collection.Find(filterDefinition).ToListAsync().ConfigureAwait(false);
return objectResult;
}
/// <summary>
/// Asynchronously finds a single document that matches the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the document.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the document that matches the filter, or <c>null</c> if no document is found.</returns>
public virtual Task<TDocument> FindOneAsync(Expression<Func<TDocument, bool>> filterExpression)
{
return Task.Run(() => _collection.Find(filterExpression).FirstOrDefaultAsync());
}
/// <summary>
/// Finds a document by its unique identifier (ID).
/// </summary>
/// <param name="id">The unique identifier of the document.</param>
/// <returns>The document that matches the specified ID, or <c>null</c> if no document is found.</returns>
public virtual TDocument FindById(string id)
{
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, id);
return _collection.Find(filter).SingleOrDefault();
}
/// <summary>
/// Asynchronously finds a document by its unique identifier (ID).
/// </summary>
/// <param name="id">The unique identifier of the document.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the document that matches the specified ID, or <c>null</c> if no document is found.</returns>
public virtual Task<TDocument> FindByIdAsync(string id)
{
return Task.Run(() =>
{
var objectId = new ObjectId(id);
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, id);
return _collection.Find(filter).SingleOrDefaultAsync();
});
}
/// <summary>
/// Inserts a single document into the collection.
/// </summary>
/// <param name="document">The document to insert.</param>
public virtual void InsertOne(TDocument document)
{
_collection.InsertOne(document);
}
/// <summary>
/// Asynchronously inserts a single document into the collection.
/// </summary>
/// <param name="document">The document to insert.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public virtual Task InsertOneAsync(TDocument document)
{
return Task.Run(() => _collection.InsertOneAsync(document));
}
/// <summary>
/// Inserts multiple documents into the collection.
/// </summary>
/// <param name="documents">The collection of documents to insert.</param>
public void InsertMany(ICollection<TDocument> documents)
{
_collection.InsertMany(documents);
}
/// <summary>
/// Asynchronously inserts multiple documents into the collection.
/// </summary>
/// <param name="documents">The collection of documents to insert.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public virtual async Task InsertManyAsync(ICollection<TDocument> documents)
{
await _collection.InsertManyAsync(documents);
}
/// <summary>
/// Replaces an existing document in the collection.
/// </summary>
/// <param name="document">The document with the updated data.</param>
public void ReplaceOne(TDocument document)
{
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, document._Id);
_collection.FindOneAndReplace(filter, document);
}
/// <summary>
/// Asynchronously replaces an existing document in the collection.
/// </summary>
/// <param name="document">The document with the updated data.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public virtual async Task ReplaceOneAsync(TDocument document)
{
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, document._Id);
await _collection.FindOneAndReplaceAsync(filter, document);
}
/// <summary>
/// Deletes a single document from the collection based on the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the document to delete.</param>
public void DeleteOne(Expression<Func<TDocument, bool>> filterExpression)
{
_collection.FindOneAndDelete(filterExpression);
}
/// <summary>
/// Asynchronously deletes a single document from the collection based on the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the document to delete.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task<TDocument> DeleteOneAsync(Expression<Func<TDocument, bool>> filterExpression)
{
return await _collection.FindOneAndDeleteAsync(filterExpression);
}
/// <summary>
/// Deletes a single document from the collection based on its unique identifier (ID).
/// </summary>
/// <param name="id">The unique identifier of the document to delete.</param>
public void DeleteById(string id)
{
var objectId = new ObjectId(id);
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, id);
_collection.FindOneAndDelete(filter);
}
/// <summary>
/// Asynchronously deletes a single document from the collection based on its unique identifier (ID).
/// </summary>
/// <param name="id">The unique identifier of the document to delete.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task DeleteByIdAsync(string id)
{
return Task.Run(() =>
{
var objectId = new ObjectId(id);
var filter = Builders<TDocument>.Filter.Eq(doc => doc._Id, id);
_collection.FindOneAndDeleteAsync(filter);
});
}
/// <summary>
/// Deletes multiple documents from the collection based on the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the documents to delete.</param>
public void DeleteMany(Expression<Func<TDocument, bool>> filterExpression)
{
_collection.DeleteMany(filterExpression);
}
/// <summary>
/// Asynchronously deletes multiple documents from the collection based on the provided filter expression.
/// </summary>
/// <param name="filterExpression">A lambda expression that defines the filter criteria for the documents to delete.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task DeleteManyAsync(Expression<Func<TDocument, bool>> filterExpression)
{
return Task.Run(() => _collection.DeleteManyAsync(filterExpression));
}
}
}

View File

@ -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; }
}
/// <summary>
/// Represents the settings for Redis caching.
/// </summary>
public class CacheSettings: ICacheSettings
{
/// <summary>
/// Gets or sets the default cache duration in minutes.
/// </summary>
public int DefaultCacheDurationInMinutes { get; set; }
}
}

View File

@ -0,0 +1,43 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Core.Blueprint.Redis.Configuration
{
/// <summary>
/// Provides extension methods for registering Redis-related services in the DI container.
/// </summary>
public static class RegisterBlueprint
{
/// <summary>
/// Adds Redis caching services to the service collection.
/// </summary>
/// <param name="services">The service collection to register the services into.</param>
/// <param name="configuration">The application configuration object.</param>
/// <returns>The updated service collection.</returns>
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<IRedisCacheProvider>(provider =>
new RedisCacheProvider(redisConnectionString, provider.GetRequiredService<ILogger<RedisCacheProvider>>()));
// Get CacheSettings and register with the ICacheSettings interface
var cacheSettings = configuration.GetSection("CacheSettings").Get<CacheSettings>();
if (cacheSettings == null)
{
throw new InvalidOperationException("Redis CacheSettings section is not configured.");
}
services.AddSingleton<ICacheSettings>(cacheSettings);
return services;
}
}
}

View File

@ -0,0 +1,48 @@
namespace Core.Blueprint.Redis
{
/// <summary>
/// Interface for managing Redis cache operations.
/// </summary>
public interface IRedisCacheProvider
{
/// <summary>
/// Retrieves a cache item by its key.
/// </summary>
/// <typeparam name="T">The type of the cached item.</typeparam>
/// <param name="key">The cache key.</param>
/// <returns>The cached item, or default if not found.</returns>
ValueTask<TEntity> GetAsync<TEntity>(string key);
/// <summary>
/// Sets a cache item with the specified key and value.
/// </summary>
/// <typeparam name="T">The type of the item to cache.</typeparam>
/// <param name="key">The cache key.</param>
/// <param name="value">The item to cache.</param>
/// <param name="expiry">The optional expiration time for the cache item.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask SetAsync<TEntity>(string key, TEntity value, TimeSpan? expiry = null);
/// <summary>
/// Removes a cache item by its key.
/// </summary>
/// <param name="key">The cache key.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask RemoveAsync(string key);
/// <summary>
/// Checks if a cache item exists for the specified key.
/// </summary>
/// <param name="key">The cache key.</param>
/// <returns>True if the cache item exists; otherwise, false.</returns>
ValueTask<bool> ExistsAsync(string key);
/// <summary>
/// Refreshes the expiration time of a cache item if it exists.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="expiry">The new expiration time for the cache item.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask RefreshAsync(string key, TimeSpan? expiry = null);
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.22" />
</ItemGroup>
</Project>

View File

@ -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
{
/// <summary>
/// Helper class for generating consistent and normalized cache keys.
/// </summary>
public static class CacheKeyHelper
{
/// <summary>
/// Generates a cache key based on the instance, method name, and parameters.
/// </summary>
/// <param name="instance">The instance of the class.</param>
/// <param name="methodName">The method name related to the cache key.</param>
/// <param name="parameters">The parameters used to generate the key.</param>
/// <returns>A normalized cache key string.</returns>
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();
}
/// <summary>
/// Normalizes a parameter value for use in a cache key.
/// </summary>
/// <param name="param">The parameter to normalize.</param>
/// <returns>A normalized string representation of the parameter.</returns>
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]", "_");
}
}
}

View File

@ -0,0 +1,171 @@
using Azure.Identity;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using System.Text.Json;
namespace Core.Blueprint.Redis
{
/// <summary>
/// Redis cache provider for managing cache operations.
/// </summary>
public sealed class RedisCacheProvider : IRedisCacheProvider
{
private IDatabase _cacheDatabase = null!;
private readonly ILogger<RedisCacheProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="RedisCacheProvider"/> class.
/// </summary>
/// <param name="connectionString">The Redis connection string.</param>
/// <param name="logger">The logger instance for logging operations.</param>
/// <exception cref="ArgumentNullException">Thrown when connection string is null or empty.</exception>
public RedisCacheProvider(string connectionString, ILogger<RedisCacheProvider> logger)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentNullException(nameof(connectionString), "Redis connection string cannot be null or empty.");
_logger = logger;
_cacheDatabase = InitializeRedisAsync(connectionString).GetAwaiter().GetResult();
}
/// <summary>
/// Initializes and establishes a connection to Redis using the provided connection string.
/// </summary>
/// <param name="connectionString">The Redis connection string.</param>
/// <returns>An <see cref="IDatabase"/> instance representing the Redis cache database.</returns>
/// <exception cref="Exception">Thrown when the connection to Redis fails.</exce
async Task<IDatabase> InitializeRedisAsync(string connectionString)
{
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;
}
}
/// <summary>
/// Retrieves a cache item by its key.
/// </summary>
/// <typeparam name="T">The type of the cached item.</typeparam>
/// <param name="key">The cache key.</param>
/// <returns>The cached item of type <typeparamref name="T"/>, or default if not found.</returns>
public async ValueTask<TEntity> GetAsync<TEntity>(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<TEntity>(value);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting cache item with key {key}");
throw;
}
}
/// <summary>
/// Sets a cache item with the specified key and value.
/// </summary>
/// <typeparam name="T">The type of the item to cache.</typeparam>
/// <param name="key">The cache key.</param>
/// <param name="value">The item to cache.</param>
/// <param name="expiry">The optional expiration time for the cache item.</param>
public async ValueTask SetAsync<TEntity>(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;
}
}
/// <summary>
/// Removes a cache item by its key.
/// </summary>
/// <param name="key">The cache key.</param>
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;
}
}
/// <summary>
/// Checks if a cache item exists for the specified key.
/// </summary>
/// <param name="key">The cache key.</param>
/// <returns>True if the cache item exists; otherwise, false.</returns>
public async ValueTask<bool> 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;
}
}
/// <summary>
/// Refreshes the expiration time of a cache item if it exists.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="expiry">The new expiration time for the cache item.</param>
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;
}
}
}
}

View File

@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Core.Blueprint.SQLServer.Entities
{
/// <summary>
/// Represents the base class for SQL Server entities, providing common properties for auditing and state management.
/// </summary>
public abstract class BaseSQLAdapter : IBaseSQLAdapter
{
/// <summary>
/// Gets or sets the identifier for the entity.
/// </summary>
[Key]
[JsonPropertyName("id")]
public int Id { get; init; }
/// <summary>
/// Gets or sets the unique identifier for the entity.
/// </summary>
[JsonPropertyName("guid")]
public string Guid { get; init; }
/// <summary>
/// Gets or sets the timestamp when the entity was created.
/// Default value is the current UTC time at the moment of instantiation.
/// </summary>
[JsonPropertyName("createdAt")]
public DateTime? CreatedAt { get; init; }
/// <summary>
/// Gets or sets the identifier of the user or system that created the entity.
/// </summary>
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
/// <summary>
/// Gets or sets the timestamp when the entity was last updated.
/// Null if the entity has not been updated.
/// </summary>
[JsonPropertyName("updatedAt")]
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the identifier of the user or system that last updated the entity.
/// Null if the entity has not been updated.
/// </summary>
[JsonPropertyName("updatedBy")]
public string? UpdatedBy { get; set; }
/// <summary>
/// Gets or sets the status of the entity, indicating whether it is active, inactive, or in another state.
/// Default value is <see cref="StatusEnum.Active"/>.
/// </summary>
[JsonPropertyName("status")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public StatusEnum Status { get; set; }
protected BaseSQLAdapter()
{
Guid = System.Guid.NewGuid().ToString();
CreatedAt = DateTime.UtcNow;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
namespace Core.Blueprint.SQLServer.Entities
{
/// <summary>
/// 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.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum StatusEnum
{
/// <summary>
/// Indicates that the entity is currently active and operational.
/// </summary>
Active = 0,
/// <summary>
/// Indicates that the entity is currently inactive but still exists in the system.
/// Typically used for temporary deactivation or soft-offline states.
/// </summary>
Inactive = 1,
/// <summary>
/// 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.
/// </summary>
Deleted = 2
}
}

View File

@ -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
{
/// <summary>
/// Provides extension methods for configuring SQL Server.
/// </summary>
public static class RegisterBlueprint
{
/// <summary>
/// Configures SQL Server services, including the database context and generic repository, for dependency injection.
/// </summary>
/// <param name="services">The service collection to which the SQL Server services will be added.</param>
/// <param name="configuration">The application configuration object for accessing settings such as connection strings.</param>
/// <returns>An updated <see cref="IServiceCollection"/> with SQL Server services registered.</returns>
public static IServiceCollection AddSQLServer(this IServiceCollection services, IConfiguration configuration)
{
var chainedCredentials = new ChainedTokenCredential(
new ManagedIdentityCredential(),
new SharedTokenCacheCredential(),
new VisualStudioCredential(),
new VisualStudioCodeCredential()
);
services.AddScoped(typeof(IEntityRepository<,>), typeof(EntityRepository<,>));
return services;
}
}
}

View File

@ -0,0 +1,57 @@
using Core.Blueprint.SQLServer.Entities;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Core.Blueprint.SQLServer
{
/// <summary>
/// Defines the interface for SQL Server entities, providing common properties for auditing and state management.
/// </summary>
public interface IBaseSQLAdapter
{
/// <summary>
/// Gets or sets the identifier for the entity.
/// </summary>
[Key]
[JsonPropertyName("id")]
int Id { get; }
/// <summary>
/// Gets or sets the GUID for the entity.
/// </summary>
[JsonPropertyName("guid")]
string Guid { get; }
/// <summary>
/// Gets or sets the timestamp when the entity was created.
/// </summary>
[JsonPropertyName("createdAt")]
DateTime? CreatedAt { get; }
/// <summary>
/// Gets or sets the identifier of the user or system that created the entity.
/// </summary>
[JsonPropertyName("createdBy")]
string? CreatedBy { get; set; }
/// <summary>
/// Gets or sets the timestamp when the entity was last updated.
/// </summary>
[JsonPropertyName("updatedAt")]
DateTime? UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the identifier of the user or system that last updated the entity.
/// </summary>
[JsonPropertyName("updatedBy")]
string? UpdatedBy { get; set; }
/// <summary>
/// Gets or sets the status of the entity, indicating whether it is active, inactive, or in another state.
/// </summary>
[JsonPropertyName("status")]
[JsonConverter(typeof(JsonStringEnumConverter))]
StatusEnum Status { get; set; }
}
}

View File

@ -0,0 +1,108 @@
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace Core.Blueprint.DAL.SQLServer
{
/// <summary>
/// Defines the contract for a generic repository to manage entities in a SQL Server database.
/// </summary>
/// <typeparam name="TEntity">The type of the entity managed by the repository. Must be a class.</typeparam>
/// <typeparam name="TContext">The type of the database context used by the repository. Must inherit from <see cref="DbContext"/>.</typeparam>
public interface IEntityRepository<TEntity, TContext>
where TEntity : class
where TContext : DbContext
{
/// <summary>
/// Retrieves all entities of type <typeparamref name="T"/> from the database.
/// </summary>
/// <returns>A task representing the asynchronous operation, with a collection of entities as the result.</returns>
Task<IEnumerable<TEntity>> GetAllAsync();
/// <summary>
/// Retrieves all entities of type <typeparamref name="T"/> from the database that match a specified condition.
/// </summary>
/// <param name="predicate">An expression to filter the entities.</param>
/// <returns>A task representing the asynchronous operation, with a collection of matching entities as the result.</returns>
Task<IEnumerable<TEntity>> GetByConditionAsync(Expression<Func<TEntity, bool>> predicate);
/// <summary>
/// Retrieves a single entity of type <typeparamref name="T"/> by its identifier.
/// </summary>
/// <param name="id">The identifier of the entity to retrieve.</param>
/// <returns>A task representing the asynchronous operation, with the entity as the result, or null if not found.</returns>
Task<TEntity?> GetByIdAsync(int id);
/// <summary>
/// Retrieves the first entity of type <typeparamref name="T"/> that matches a specified condition, or null if no match is found.
/// </summary>
/// <param name="predicate">An expression to filter the entities.</param>
/// <returns>A task representing the asynchronous operation, with the matching entity as the result, or null if none match.</returns>
Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
/// <summary>
/// Adds a new entity of type <typeparamref name="T"/> to the database.
/// </summary>
/// <param name="entity">The entity to add.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddAsync(TEntity entity);
/// <summary>
/// Adds multiple entities of type <typeparamref name="T"/> to the database.
/// </summary>
/// <param name="entities">The collection of entities to add.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddRangeAsync(IEnumerable<TEntity> entities);
/// <summary>
/// Updates an existing entity of type <typeparamref name="T"/> in the database.
/// </summary>
/// <param name="entity">The entity to update.</param>
/// <returns>The updated entity.</returns>
TEntity Update(TEntity entity);
/// <summary>
/// Updates multiple entities of type <typeparamref name="T"/> in the database.
/// </summary>
/// <param name="entities">The collection of entities to update.</param>
void UpdateRange(IEnumerable<TEntity> entities);
/// <summary>
/// Deletes an entity of type <typeparamref name="T"/> from the database.
/// </summary>
/// <param name="entity">The entity to delete.</param>
void Delete(TEntity entity);
/// <summary>
/// Deletes multiple entities of type <typeparamref name="T"/> from the database.
/// </summary>
/// <param name="entities">The collection of entities to delete.</param>
void DeleteRange(IEnumerable<TEntity> entities);
/// <summary>
/// Determines whether any entities of type <typeparamref name="T"/> exist in the database that match a specified condition.
/// </summary>
/// <param name="predicate">An expression to filter the entities.</param>
/// <returns>A task representing the asynchronous operation, with a boolean result indicating whether any match exists.</returns>
Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate);
/// <summary>
/// Executes a raw SQL query and maps the result to entities of type <typeparamref name="T"/>.
/// </summary>
/// <param name="sql">The raw SQL query to execute.</param>
/// <param name="parameters">Optional parameters for the SQL query.</param>
/// <returns>A task representing the asynchronous operation, with a collection of entities as the result.</returns>
Task<IEnumerable<TEntity>> ExecuteRawSqlAsync(string sql, params object[] parameters);
/// <summary>
/// Counts the total number of entities of type <typeparamref name="T"/> in the database.
/// </summary>
/// <returns>A task representing the asynchronous operation, with the count as the result.</returns>
Task<int> CountAsync();
/// <summary>
/// Saves all pending changes to the database.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
Task SaveAsync();
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,182 @@
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace Core.Blueprint.DAL.SQLServer
{
/// <summary>
/// The <see cref="EntityRepository{TEntity, TContext}"/> 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.
/// </summary>
/// <typeparam name="TEntity">The entity type managed by the repository. Must be a class.</typeparam>
/// <typeparam name="TContext">The database context type. Must inherit from <see cref="DbContext"/>.</typeparam>
public class EntityRepository<TEntity, TContext> : IEntityRepository<TEntity, TContext>
where TEntity : class
where TContext : DbContext
{
private readonly TContext _context;
private readonly DbSet<TEntity> _dbSet;
/// <summary>
/// Initializes a new instance of the <see cref="EntityRepository{TEntity, TContext}"/> class with a specified database context.
/// </summary>
/// <param name="context">The <see cref="TContext"/> for database operations.</param>
public EntityRepository(TContext context)
{
_context = context;
_dbSet = _context.Set<TEntity>();
}
/// <summary>
/// Retrieves all entities of type <typeparamref name="TEntity"/> from the database.
/// </summary>
/// <returns>A task representing the asynchronous operation, with a list of entities as the result.</returns>
public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
/// <summary>
/// Retrieves all entities of type <typeparamref name="TEntity"/> from the database that match a specified filter.
/// </summary>
/// <param name="predicate">An expression to filter entities.</param>
/// <returns>A task representing the asynchronous operation, with a list of filtered entities as the result.</returns>
public async Task<IEnumerable<TEntity>> GetByConditionAsync(Expression<Func<TEntity, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
/// <summary>
/// Retrieves a single entity of type <typeparamref name="TEntity"/> by its identifier.
/// </summary>
/// <param name="id">The identifier of the entity.</param>
/// <returns>A task representing the asynchronous operation, with the entity as the result, or null if not found.</returns>
public async Task<TEntity?> GetByIdAsync(int id)
{
var existingEntity = await _dbSet.FindAsync(id);
if (existingEntity != null)
{
_context.Entry(existingEntity).State = EntityState.Detached;
}
return existingEntity;
}
/// <summary>
/// Adds a new entity to the database.
/// </summary>
/// <param name="entity">The entity to add.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task AddAsync(TEntity entity)
{
await _dbSet.AddAsync(entity);
}
/// <summary>
/// Adds multiple entities to the database.
/// </summary>
/// <param name="entities">The collection of entities to add.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task AddRangeAsync(IEnumerable<TEntity> entities)
{
await _dbSet.AddRangeAsync(entities);
}
/// <summary>
/// Updates an existing entity in the database.
/// </summary>
/// <param name="entity">The entity to update.</param>
/// <returns>The updated entity.</returns>
public TEntity Update(TEntity entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
return entity;
}
/// <summary>
/// Updates multiple entities in the database.
/// </summary>
/// <param name="entities">The collection of entities to update.</param>
public void UpdateRange(IEnumerable<TEntity> entities)
{
foreach (var entity in entities)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
}
/// <summary>
/// Deletes an entity from the database.
/// </summary>
/// <param name="entity">The entity to delete.</param>
public void Delete(TEntity entity)
{
if (_context.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
/// <summary>
/// Deletes multiple entities from the database.
/// </summary>
/// <param name="entities">The collection of entities to delete.</param>
public void DeleteRange(IEnumerable<TEntity> entities)
{
_dbSet.RemoveRange(entities);
}
/// <summary>
/// Retrieves the first entity matching the specified condition or null if no match is found.
/// </summary>
/// <param name="predicate">An expression to filter entities.</param>
/// <returns>A task representing the asynchronous operation, with the matched entity as the result.</returns>
public async Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
{
return await _dbSet.FirstOrDefaultAsync(predicate);
}
/// <summary>
/// Determines if any entities exist that match the specified condition.
/// </summary>
/// <param name="predicate">An expression to filter entities.</param>
/// <returns>A task representing the asynchronous operation, with a boolean result indicating existence.</returns>
public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate)
{
return await _dbSet.AnyAsync(predicate);
}
/// <summary>
/// Saves all pending changes to the database.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task SaveAsync()
{
await _context.SaveChangesAsync();
}
/// <summary>
/// Executes a raw SQL query and maps the result to the specified entity type.
/// </summary>
/// <param name="sql">The raw SQL query.</param>
/// <param name="parameters">Optional parameters for the query.</param>
/// <returns>An <see cref="IEnumerable{TEntity}"/> representing the result set.</returns>
public async Task<IEnumerable<TEntity>> ExecuteRawSqlAsync(string sql, params object[] parameters)
{
return await _dbSet.FromSqlRaw(sql, parameters).ToListAsync();
}
/// <summary>
/// Counts the total number of entities in the database.
/// </summary>
/// <returns>A task representing the asynchronous operation, with the count as the result.</returns>
public async Task<int> CountAsync()
{
return await _dbSet.CountAsync();
}
}
}

View File

@ -0,0 +1,8 @@
namespace Core.Blueprint.Storage
{
public class BlobAddDto
{
public string? FileName { get; set; }
public byte[] FileContent { get; set; } = null!;
}
}

View File

@ -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
{
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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<BlobStorageFolder> SubFolders { get; set; } = [];
public List<BlobStorageFilesAdapter> Files { get; set; } = [];
}
public record BlobStorageFilesAdapter(string Content, string Name, string ContentType, string DownloadUrl);
}

View File

@ -0,0 +1,66 @@

namespace Core.Blueprint.Storage
{
public class TrieNode
{
public Dictionary<char, TrieNode> 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<string> SearchByPrefix(string? prefix)
{
var results = new List<string>();
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<string> results)
{
if (node.IsEndOfWord)
{
results.Add(currentPrefix);
}
foreach (var kvp in node.Children)
{
SearchByPrefixHelper(kvp.Value, currentPrefix + kvp.Key, results);
}
}
}
}

View File

@ -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<IBlobStorageProvider, BlobStorageProvider>();
return services;
}
}
}

View File

@ -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
{
/// <summary>
/// Defines a contract for managing blobs and containers in Azure Blob Storage.
/// </summary>
public interface IBlobStorageProvider
{
/// <summary>
/// Creates the blob container if it does not exist.
/// </summary>
/// <returns>A <see cref="Response{T}"/> containing the container information.</returns>
Task<Response<BlobContainerInfo>> CreateIfNotExistsAsync();
/// <summary>
/// Deletes the blob container if it exists.
/// </summary>
Task DeleteIfExistsAsync();
/// <summary>
/// Gets properties of the blob container.
/// </summary>
/// <returns>A <see cref="Response{T}"/> containing container properties.</returns>
Task<Response<BlobContainerProperties>> GetPropertiesAsync();
/// <summary>
/// Sets metadata for the blob container.
/// </summary>
/// <param name="metadata">The metadata to set for the container.</param>
Task SetMetadataAsync(IDictionary<string, string> metadata);
/// <summary>
/// Uploads a blob to the container.
/// </summary>
/// <param name="blobName">The name of the blob.</param>
/// <param name="content">The content to upload.</param>
/// <returns>A <see cref="Response{T}"/> containing blob content information.</returns>
Task<Response<BlobContentInfo>> UploadBlobAsync(string blobName, Stream content);
/// <summary>
/// Downloads a blob from the container.
/// </summary>
/// <param name="blobName">The name of the blob.</param>
/// <returns>A <see cref="Response{T}"/> containing blob download information.</returns>
/// <exception cref="FileNotFoundException">Thrown if the blob does not exist.</exception>
Task<Response<BlobDownloadInfo>> DownloadBlobAsync(string blobName);
/// <summary>
/// Deletes a blob from the container.
/// </summary>
/// <param name="blobName">The name of the blob.</param>
Task<bool> DeleteBlobAsync(string blobName);
/// <summary>
/// Lists all blobs in the container with an optional prefix.
/// </summary>
/// <param name="prefix">The prefix to filter blobs.</param>
/// <returns>A collection of <see cref="BlobItem"/>.</returns>
Task<IEnumerable<BlobItem>> ListBlobItemAsync(string? prefix = null);
/// <summary>
/// Retrieves the account information for the associated Blob Service Client.
/// </summary>
/// <param name="cancellation">
/// A <see cref="CancellationToken"/> that can be used to cancel the operation.
/// </param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the
/// <see cref="AccountInfo"/> object, which provides details about the account, such as the SKU
/// and account kind.
Task<AccountInfo> GetAccountInfoAsync(CancellationToken cancellation);
/// <summary>
/// Gets a blob client for a specific blob.
/// </summary>
/// <param name="blobName">The name of the blob.</param>
/// <returns>A <see cref="BlobClient"/> for the blob.</returns>
BlobClient GetBlobClient(string blobName);
/// <summary>
/// Lists blobs hierarchically using a delimiter.
/// </summary>
/// <param name="prefix">The prefix to filter blobs.</param>
/// <param name="delimiter">The delimiter to use for hierarchy.</param>
/// <returns>A collection of <see cref="BlobHierarchyItem"/>.</returns>
Task<IEnumerable<BlobHierarchyItem>> ListBlobsByHierarchyAsync(string? prefix = null, string delimiter = "/");
/// <summary>
/// Generates a SAS token for the container with specified permissions.
/// </summary>
/// <param name="permissions">The permissions to assign to the SAS token.</param>
/// <param name="expiresOn">The expiration time for the SAS token.</param>
/// <returns>A <see cref="Uri"/> containing the SAS token.</returns>
/// <exception cref="InvalidOperationException">Thrown if SAS URI generation is not supported.</exception>
Uri GenerateContainerSasUri(BlobContainerSasPermissions permissions, DateTimeOffset expiresOn);
/// <summary>
/// Acquires a lease on the blob container.
/// </summary>
/// <param name="proposedId">The optional proposed lease ID.</param>
/// <param name="duration">The optional lease duration.</param>
/// <returns>A <see cref="Response{T}"/> containing lease information.</returns>
Task<Response<BlobLease>> AcquireLeaseAsync(string? proposedId = null, TimeSpan? duration = null);
/// <summary>
/// Releases a lease on the blob container.
/// </summary>
/// <param name="leaseId">The lease ID to release.</param>
Task ReleaseLeaseAsync(string leaseId);
/// <summary>
/// Sets access policies for the blob container.
/// </summary>
/// <param name="accessType">The type of public access to allow.</param>
/// <param name="identifiers">The optional list of signed identifiers for access policy.</param>
Task SetAccessPolicyAsync(PublicAccessType accessType, IEnumerable<BlobSignedIdentifier>? identifiers = null);
/// <summary>
/// Lists blobs in the container with an optional prefix.
/// </summary>
/// <param name="prefix">The prefix to filter blobs.</param>
/// <returns>A collection of <see cref="BlobFileAdapter"/>.</returns>
Task<IEnumerable<BlobFileAdapter>> ListBlobsAsync(string? prefix = null);
/// <summary>
/// Uploads a blob to the container.
/// </summary>
/// <param name="newBlob">The blob to upload.</param>
/// <returns>A <see cref="BlobFileAdapter"/> representing the uploaded blob.</returns>
Task<BlobFileAdapter> UploadBlobAsync(BlobAddDto newBlob);
/// <summary>
/// Deletes a blob from the container.
/// </summary>
/// <param name="fileName">The name of the blob to delete.</param>
/// <returns>A <see cref="BlobFileAdapter"/> representing the deleted blob, or null if the blob was not found.</returns>
Task<BlobFileAdapter?> DeleteBlobsAsync(string fileName);
/// <summary>
/// Downloads a blob's content.
/// </summary>
/// <param name="blobName">The name of the blob.</param>
/// <returns>A <see cref="BlobDownloadInfo"/> representing the downloaded blob.</returns>
/// <exception cref="FileNotFoundException">Thrown if the blob does not exist.</exception>
Task<BlobDownloadInfo> DownloadBlobsAsync(string blobName);
/// <summary>
/// Generates a secure download URI for a specified blob in the storage container.
/// </summary>
/// <param name="blobName">The name of the blob for which the download URI is being generated.</param>
/// <returns>
/// An instance of <see cref="BlobDownloadUriAdapter"/> containing the generated URI, blob name, and status.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="blobName"/> is null or empty.</exception>
/// <exception cref="StorageException">Thrown if there is an issue communicating with the Azure Blob service.</exception>
BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName);
/// <summary>
/// Retrieves the hierarchical folder structure.
/// </summary>
/// <param name="prefix">The prefix to start the hierarchy retrieval.</param>
/// <returns>A list of <see cref="BlobStorageFolder"/> representing the folder structure.</returns>
Task<List<BlobStorageFolder>> GetFolderHierarchyAsync(string prefix);
/// <summary>
/// Lists neighboring folders based on a prefix.
/// </summary>
/// <param name="prefix">The prefix to search for neighboring folders.</param>
/// <returns>A dictionary grouping folder names by their prefix.</returns>
Task<Dictionary<string, List<string>>> ListNeighborFoldersAsync(string? prefix);
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.9.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@ -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);
}
/// <summary>
/// Creates the blob container if it does not exist.
/// </summary>
public async Task<Response<BlobContainerInfo>> CreateIfNotExistsAsync()
{
return await _blobContainerClient.CreateIfNotExistsAsync();
}
/// <summary>
/// Deletes the blob container if it exists.
/// </summary>
public async Task DeleteIfExistsAsync()
{
await _blobContainerClient.DeleteIfExistsAsync();
}
/// <summary>
/// Gets properties of the blob container.
/// </summary>
public async Task<Response<BlobContainerProperties>> GetPropertiesAsync()
{
return await _blobContainerClient.GetPropertiesAsync();
}
/// <summary>
/// Sets metadata for the blob container.
/// </summary>
public async Task SetMetadataAsync(IDictionary<string, string> metadata)
{
await _blobContainerClient.SetMetadataAsync(metadata);
}
/// <summary>
/// Uploads a blob to the container.
/// </summary>
public async Task<Response<BlobContentInfo>> UploadBlobAsync(string blobName, Stream content)
{
var blobClient = _blobContainerClient.GetBlobClient(blobName);
return await blobClient.UploadAsync(content, overwrite: true);
}
/// <summary>
/// Downloads a blob from the container.
/// </summary>
public async Task<Response<BlobDownloadInfo>> DownloadBlobAsync(string blobName)
{
var blobClient = _blobContainerClient.GetBlobClient(blobName);
return await blobClient.DownloadAsync();
}
/// <summary>
/// Deletes a blob from the container.
/// </summary>
public async Task<bool> DeleteBlobAsync(string blobName)
{
var blobClient = _blobContainerClient.GetBlobClient(blobName);
return await blobClient.DeleteIfExistsAsync();
}
/// <summary>
/// Lists all blobs in the container with an optional prefix.
/// </summary>
public async Task<IEnumerable<BlobItem>> ListBlobItemAsync(string? prefix = null)
{
var blobs = new List<BlobItem>();
await foreach (var blobItem in _blobContainerClient.GetBlobsAsync(prefix: prefix))
{
blobs.Add(blobItem);
}
return blobs;
}
/// <summary>
/// Retrieves the account information for the associated Blob Service Client.
/// </summary>
/// <param name="cancellation">
/// A <see cref="CancellationToken"/> that can be used to cancel the operation.
/// </param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the
/// <see cref="AccountInfo"/> object, which provides details about the account, such as the SKU
/// and account kind.
public async Task<AccountInfo> GetAccountInfoAsync(CancellationToken cancellation)
{
return await _blobServiceClient.GetAccountInfoAsync(cancellation);
}
/// <summary>
/// Gets a blob client for a specific blob.
/// </summary>
public BlobClient GetBlobClient(string blobName)
{
return _blobContainerClient.GetBlobClient(blobName);
}
/// <summary>
/// Lists blobs hierarchically using a delimiter.
/// </summary>
public async Task<IEnumerable<BlobHierarchyItem>> ListBlobsByHierarchyAsync(string? prefix = null, string delimiter = "/")
{
var blobs = new List<BlobHierarchyItem>();
await foreach (var blobHierarchyItem in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: delimiter))
{
blobs.Add(blobHierarchyItem);
}
return blobs;
}
/// <summary>
/// Generates a SAS token for the container with specified permissions.
/// </summary>
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);
}
/// <summary>
/// Acquires a lease on the blob container.
/// </summary>
public async Task<Response<BlobLease>> AcquireLeaseAsync(string? proposedId = null, TimeSpan? duration = null)
{
return await _blobContainerClient.GetBlobLeaseClient(proposedId).AcquireAsync(duration ?? TimeSpan.FromSeconds(60));
}
/// <summary>
/// Releases a lease on the blob container.
/// </summary>
public async Task ReleaseLeaseAsync(string leaseId)
{
await _blobContainerClient.GetBlobLeaseClient(leaseId).ReleaseAsync();
}
/// <summary>
/// Sets access policies for the blob container.
/// </summary>
public async Task SetAccessPolicyAsync(PublicAccessType accessType, IEnumerable<BlobSignedIdentifier>? identifiers = null)
{
await _blobContainerClient.SetAccessPolicyAsync(accessType, identifiers);
}
/// <summary>
/// Lists blobs in the container with an optional prefix.
/// </summary>
public async Task<IEnumerable<BlobFileAdapter>> ListBlobsAsync(string? prefix = null)
{
var blobs = new List<BlobFileAdapter>();
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;
}
/// <summary>
/// Uploads a blob to the container.
/// </summary>
public async Task<BlobFileAdapter> 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"
};
}
/// <summary>
/// Deletes a blob from the container.
/// </summary>
public async Task<BlobFileAdapter?> 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;
}
/// <summary>
/// Downloads a blob's content.
/// </summary>
public async Task<BlobDownloadInfo> 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();
}
/// <summary>
/// Generates a secure download URI for a specified blob in the storage container.
/// </summary>
/// <param name="blobName">The name of the blob for which the download URI is being generated.</param>
/// <returns>
/// An instance of <see cref="BlobDownloadUriAdapter"/> containing the generated URI, blob name, and status.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="blobName"/> is null or empty.</exception>
/// <exception cref="StorageException">Thrown if there is an issue communicating with the Azure Blob service.</exception>
public BlobDownloadUriAdapter GenerateBlobDownloadUri(string blobName)
{
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"
};
}
/// <summary>
/// Retrieves the hierarchical folder structure.
/// </summary>
public async Task<List<BlobStorageFolder>> GetFolderHierarchyAsync(string prefix)
{
var rootFolder = new BlobStorageFolder { Name = prefix };
await PopulateFolderAsync(rootFolder, prefix);
return new List<BlobStorageFolder> { 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<Dictionary<string, List<string>>> 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);
}
}
}
}
}