Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions .github/workflows/projectdeploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,40 @@ on:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:

jobs:
build-push-deploy:
runs-on: "self-hosted"
steps:
-
name: checkout
- name: Checkout
uses: actions/checkout@v3
-
name: get tag name
- name: Get tag name
run: echo "TAG_ENV=$(date +'%Y-%m-%dT%H%M%S')" >> "$GITHUB_ENV"
-
name: build
run: docker build -f ./Dockerfile -t databaseprojectapi:${{env.TAG_ENV}} .
-
name: tag
- name: Build
run: docker build -f ./Dockerfile -t databaseprojectapi:${{env.TAG_ENV}} .
- name: Tag
run: |
docker tag databaseprojectapi:${{env.TAG_ENV}} reg.dqcsapp.com/databaseprojectapi:${{env.TAG_ENV}}
docker tag databaseprojectapi:${{env.TAG_ENV}} reg.dqcsapp.com/databaseprojectapi:latest
-
name: push
- name: Push
run: |
docker push reg.dqcsapp.com/databaseprojectapi:${{env.TAG_ENV}}
docker push reg.dqcsapp.com/databaseprojectapi:latest
-
name: deploy kubernetes set image

approval:
needs: build-push-deploy
runs-on: ubuntu-latest
steps:
- name: Awaiting Manual Approval
run: echo "Waiting for manual approval. Go to GitHub Actions UI and approve the job."

Comment on lines +34 to +35
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approval job does not implement actual approval logic. A simple echo statement will not pause the workflow for manual approval. Consider using GitHub's environment feature with protection rules or the trstringer/manual-approval action to implement proper manual approval gates.

Suggested change
run: echo "Waiting for manual approval. Go to GitHub Actions UI and approve the job."
uses: trstringer/manual-approval@v1
with:
prompt: "Please approve this deployment to proceed."

Copilot uses AI. Check for mistakes.
deploy:
needs: approval
runs-on: "self-hosted"
steps:
- name: Deploy Kubernetes Set Image
run: |
kubectl set image deployments/databaseprojectapi databaseprojectapi=reg.dqcsapp.com/databaseprojectapi:${{env.TAG_ENV}} --kubeconfig=/etc/kube/config
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TAG_ENV environment variable is set in the build-push-deploy job but is referenced in the deploy job. Environment variables do not persist across different jobs. You need to use job outputs or artifacts to pass the tag value between jobs.

Copilot uses AI. Check for mistakes.
-
name: remove local images
run : docker rmi -f $(docker images -aq)
- name: Remove Local Images
run: docker rmi -f $(docker images -aq)
9 changes: 9 additions & 0 deletions DatabaseProjectAPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseProjectAPI", "Datab
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KubsConnect", "KubsConnect\KubsConnect.csproj", "{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnitTest", "XUnitTest\XUnitTest.csproj", "{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}"
ProjectSection(ProjectDependencies) = postProject
{BCFC56D8-061F-4C1C-AA30-41E784E4AE13} = {BCFC56D8-061F-4C1C-AA30-41E784E4AE13}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +26,10 @@ Global
{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}.Release|Any CPU.Build.0 = Release|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
131 changes: 64 additions & 67 deletions DatabaseProjectAPI/Actions/AutoDeleteAction.cs
Original file line number Diff line number Diff line change
@@ -1,91 +1,88 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using DatabaseProjectAPI.DataContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using DatabaseProjectAPI.DataContext;

namespace DatabaseProjectAPI.Actions
namespace DatabaseProjectAPI.Actions;

public interface IAutoDeleteService
{
Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken);
Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken);
}

public class AutoDeleteAction : IAutoDeleteService
{
public interface IAutoDeleteService
private readonly DpapiDbContext _dbContext;
private readonly ILogger<AutoDeleteAction> _logger;

public AutoDeleteAction(DpapiDbContext dbContext, ILogger<AutoDeleteAction> logger)
{
Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken);
Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken);
_dbContext = dbContext;
_logger = logger;
}

public class AutoDeleteAction : IAutoDeleteService
public async Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken)
{
private readonly DpapiDbContext _dbContext;
private readonly ILogger<AutoDeleteAction> _logger;
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);

public AutoDeleteAction(DpapiDbContext dbContext, ILogger<AutoDeleteAction> logger)
try
{
_dbContext = dbContext;
_logger = logger;
}

var oldHistories = await _dbContext.StockHistories
.Where(sh => sh.Timestamp < ninetyDaysAgo)
.ToListAsync(cancellationToken);

public async Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken)
{
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);

try
{

int deletedCount = await _dbContext.StockHistories
.Where(sh => sh.Timestamp < ninetyDaysAgo)
.ExecuteDeleteAsync(cancellationToken);

if (deletedCount > 0)
{
_logger.LogInformation("{Count} old stock history records deleted.", deletedCount);
}
else
{
_logger.LogInformation("No stock history records found to delete.");
}
}
catch (OperationCanceledException)
if (oldHistories.Any())
{
_logger.LogInformation("Deletion of old stock history was cancelled.");
throw;
_dbContext.StockHistories.RemoveRange(oldHistories);
await _dbContext.SaveChangesAsync(cancellationToken);
Comment on lines +29 to +36
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from ExecuteDeleteAsync to ToListAsync().RemoveRange().SaveChanges() significantly impacts performance. This loads all records into memory before deletion, which is inefficient for large datasets. If this change was made solely for testability, consider using a repository pattern or keeping ExecuteDeleteAsync in production while mocking the DbContext properly in tests.

Copilot uses AI. Check for mistakes.
_logger.LogInformation("{Count} old stock history records deleted.", oldHistories.Count);
}
catch (Exception ex)
else
{
_logger.LogError(ex, "An error occurred while deleting old stock history records.");
throw;
_logger.LogInformation("No stock history records found to delete.");
}
}

public async Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken)
catch (OperationCanceledException)
{
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);
_logger.LogInformation("Deletion of old stock history was cancelled.");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while deleting old stock history records.");
throw;
}
}

try
{
// Use ExecuteDeleteAsync for efficient bulk deletion (EF Core 7.0+)
int deletedCount = await _dbContext.ApiCallLog
.Where(log => log.CallDate < ninetyDaysAgo)
.ExecuteDeleteAsync(cancellationToken);
public async Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken)
{
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);

if (deletedCount > 0)
{
_logger.LogInformation("{Count} old API call log records deleted.", deletedCount);
}
else
{
_logger.LogInformation("No API call log records found to delete.");
}
}
catch (OperationCanceledException)
try
{
var oldLogs = await _dbContext.ApiCallLog
.Where(log => log.CallDate < ninetyDaysAgo)
.ToListAsync(cancellationToken);

if (oldLogs.Any())
{
_logger.LogInformation("Deletion of old API call logs was cancelled.");
throw;
_dbContext.ApiCallLog.RemoveRange(oldLogs);
await _dbContext.SaveChangesAsync(cancellationToken);
Comment on lines +62 to +69
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from ExecuteDeleteAsync to ToListAsync().RemoveRange().SaveChanges() significantly impacts performance. This loads all records into memory before deletion, which is inefficient for large datasets. If this change was made solely for testability, consider using a repository pattern or keeping ExecuteDeleteAsync in production while mocking the DbContext properly in tests.

Copilot uses AI. Check for mistakes.
_logger.LogInformation("{Count} old API call log records deleted.", oldLogs.Count);
}
catch (Exception ex)
else
{
_logger.LogError(ex, "An error occurred while deleting old API call log records.");
throw;
_logger.LogInformation("No API call log records found to delete.");
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Deletion of old API call logs was cancelled.");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while deleting old API call log records.");
throw;
}
}
}
1 change: 1 addition & 0 deletions DatabaseProjectAPI/DatabaseProjectAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
Expand Down
46 changes: 27 additions & 19 deletions DatabaseProjectAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@
using DatabaseProjectAPI.Services;
using KubsConnect;
using DatabaseProjectAPI.Helpers;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// Load configuration sources
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddUserSecrets<Program>(optional: true)
.AddEnvironmentVariables();

// Configure settings
//builder.Services.Configure<FinnhubSettings>(builder.Configuration.GetSection("Finnhub"));
//builder.Services.Configure<AlphaVantageSettings>(builder.Configuration.GetSection("AlphaVantage"));
//builder.Services.Configure<NewsSettings>(builder.Configuration.GetSection("NewsAPI"));
KubsClient client = new KubsClient(builder.Services, builder.Configuration, builder.Environment);

// Configure DbContext
//var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
string UserSecretsId = GetUserSecretsId();
KubsClient client;
if (UserSecretsId != null)
{
builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true);
var config = new StartupConfig();
builder.Configuration.GetSection("StartupConfig").Bind(config);
client = new KubsClient(builder.Services, config);
}
else
{
client = new KubsClient(builder.Services, builder.Configuration, builder.Environment);
}

builder.Services.AddDbContext<DpapiDbContext>(options =>
{
options.UseMySql(client.config.DBConnectionSettings.RyanWilliamDB,
ServerVersion.AutoDetect(client.config.DBConnectionSettings.RyanWilliamDB));
options.UseMySql(client.config.DBConnectionSettings.MySqlDB,
ServerVersion.AutoDetect(client.config.DBConnectionSettings.MySqlDB));
});

// Register services
builder.Services.AddHealthChecks()
.AddMySql(client.config.DBConnectionSettings.RyanWilliamDB, name: "MySQL Database");
.AddMySql(client.config.DBConnectionSettings.MySqlDB, name: "MySQL Database");

builder.Services.AddHttpClient<IFinnhubService, FinnhubService>();
builder.Services.AddHttpClient<IAlphaVantageService, AlphaVantageService>();
Expand All @@ -52,7 +52,6 @@
builder.Services.AddHostedService<NewsBackgroundService>();
builder.Services.AddHostedService<EventsBackgroundService>();


builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Expand All @@ -69,4 +68,13 @@
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
app.Run();

string GetUserSecretsId()
{
var attribute = typeof(Program).Assembly
.GetCustomAttribute(typeof(Microsoft.Extensions.Configuration.UserSecrets.UserSecretsIdAttribute))
as Microsoft.Extensions.Configuration.UserSecrets.UserSecretsIdAttribute;

return attribute?.UserSecretsId;
}
2 changes: 1 addition & 1 deletion DatabaseProjectAPI/Services/StockQuoteBackgroundService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
private async Task FetchAndSaveStockDataAsync(DpapiDbContext dbContext, IApiRequestLogger apiRequestLogger, string callType, TrackedStock trackedStock, CancellationToken cancellationToken)
public async Task FetchAndSaveStockDataAsync(DpapiDbContext dbContext, IApiRequestLogger apiRequestLogger, string callType, TrackedStock trackedStock, CancellationToken cancellationToken)
{
var symbol = trackedStock.Symbol;
_logger.LogInformation("FetchAndSaveStockDataAsync started for symbol {Symbol} with call type {CallType}", symbol, callType);
Expand Down
11 changes: 1 addition & 10 deletions DatabaseProjectAPI/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
{
"Finnhub": {
"ApiKey": "from-env"
},
"AlphaVantage": {
"ApiKey": "from-env"
},
"NewsAPI": {
"ApiKey": "from-env"
},
"Logging": {
"LogLevel": {
"Default": "Information",
Expand All @@ -16,4 +7,4 @@
}
},
"AllowedHosts": "*"
}
}
26 changes: 24 additions & 2 deletions KubsConnect/KubsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ namespace KubsConnect;

public class KubsClient : IKubsClient
{
string K8SNameSpace = "default";
private string K8SNameSpace = "default";
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field 'K8SNameSpace' can be 'readonly'.

Suggested change
private string K8SNameSpace = "default";
private readonly string K8SNameSpace = "default";

Copilot uses AI. Check for mistakes.

/// <summary>
/// Constructor that will be used when running in a kubernetes cluster
/// </summary>
/// <param name="pservices"></param>
/// <param name="pconfig"></param>
/// <param name="env"></param>
public KubsClient(IServiceCollection pservices, Microsoft.Extensions.Configuration.IConfiguration pconfig, IWebHostEnvironment env)
{
if (env.EnvironmentName.ToLower() != "prod")
Expand All @@ -25,6 +32,22 @@ public KubsClient(IServiceCollection pservices, Microsoft.Extensions.Configurati
this.config!.AddClassToServices(pservices);
}

/// <summary>
/// Constrictor that will only be used when user secrets is available
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Constrictor' to 'Constructor'.

Suggested change
/// Constrictor that will only be used when user secrets is available
/// Constructor that will only be used when user secrets is available

Copilot uses AI. Check for mistakes.
/// </summary>
/// <param name="pservices"></param>
/// <param name="pconfig"></param>
/// <exception cref="Exception"></exception>
public KubsClient(IServiceCollection pservices, StartupConfig? pconfig)
{
if (pconfig == null)
{
throw new Exception("StartupConfig is null");
}
this.config = pconfig;
this.config!.AddClassToServices(pservices);
}

public StartupConfig? config { get; set; }
public KubernetesClientConfiguration? K8sConfig { get; set; }
public Kubernetes? client { get; set; }
Expand Down Expand Up @@ -57,7 +80,6 @@ public KubsClient(IServiceCollection pservices, Microsoft.Extensions.Configurati
}
else
{

return JsonConvert.DeserializeObject<T>(secret);
}
}
Expand Down
Loading
Loading