Files
managing-apps/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs
cryptooda 1d33c6c2ee Update AgentSummaryRepository to clarify BacktestCount management
- Added comments to indicate that BacktestCount is not updated directly in the entity, as it is managed independently via IncrementBacktestCountAsync. This change prevents other update operations from overwriting the BacktestCount, ensuring data integrity.
2026-01-09 04:27:58 +07:00

556 lines
22 KiB
C#

using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Statistics;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class AgentSummaryRepository : IAgentSummaryRepository
{
private readonly ManagingDbContext _context;
private readonly ILogger<AgentSummaryRepository> _logger;
public AgentSummaryRepository(ManagingDbContext context, ILogger<AgentSummaryRepository> logger)
{
_context = context;
_logger = logger;
}
public async Task<AgentSummary?> GetByUserIdAsync(int userId)
{
var entity = await _context.AgentSummaries
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.UserId == userId);
return entity != null ? MapToDomain(entity) : null;
}
public async Task<AgentSummary?> GetByAgentNameAsync(string agentName)
{
var entity = await _context.AgentSummaries
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.AgentName == agentName);
return entity != null ? MapToDomain(entity) : null;
}
public async Task<IEnumerable<AgentSummary>> GetAllAsync()
{
var entities = await _context.AgentSummaries
.Include(a => a.User)
.ToListAsync();
return entities.Select(MapToDomain);
}
public async Task InsertAsync(AgentSummary agentSummary)
{
var entity = MapToEntity(agentSummary);
entity.CreatedAt = DateTime.UtcNow;
entity.UpdatedAt = DateTime.UtcNow;
await _context.AgentSummaries.AddAsync(entity);
await _context.SaveChangesAsync();
_logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
}
public async Task UpdateAsync(AgentSummary agentSummary)
{
var entity = await _context.AgentSummaries
.FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId);
if (entity != null)
{
MapToEntity(agentSummary, entity);
entity.UpdatedAt = DateTime.UtcNow;
// No need to call Update() since the entity is already being tracked
await _context.SaveChangesAsync();
_logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
}
}
public async Task SaveOrUpdateAsync(AgentSummary agentSummary)
{
// Use the execution strategy to handle retries and transactions
var strategy = _context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Ensure the User entity exists and is saved
if (agentSummary.User != null)
{
var existingUser = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == agentSummary.UserId);
if (existingUser == null)
{
// User doesn't exist, save it first
var userEntity = PostgreSqlMappers.Map(agentSummary.User);
userEntity.Id = agentSummary.UserId; // Ensure the ID is set
await _context.Users.AddAsync(userEntity);
await _context.SaveChangesAsync();
_logger.LogInformation("User created for AgentSummary with ID {UserId}", agentSummary.UserId);
}
}
// Check if AgentSummary exists (using AsNoTracking to avoid tracking conflicts)
var existing = await _context.AgentSummaries
.AsNoTracking()
.FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId);
if (existing == null)
{
// Insert new AgentSummary
var entity = MapToEntity(agentSummary);
entity.CreatedAt = DateTime.UtcNow;
entity.UpdatedAt = DateTime.UtcNow;
await _context.AgentSummaries.AddAsync(entity);
await _context.SaveChangesAsync();
_logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
}
else
{
// Update existing record - check if already tracked first
var entityToUpdate = MapToEntity(agentSummary);
entityToUpdate.Id = existing.Id; // Preserve the existing ID
entityToUpdate.CreatedAt = existing.CreatedAt; // Preserve creation date
entityToUpdate.UpdatedAt = DateTime.UtcNow;
// Check if an entity with this key is already being tracked
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
.FirstOrDefault(e => e.Entity.Id == existing.Id);
if (trackedEntity != null)
{
// Entity is already tracked, update its values
MapToEntity(agentSummary, trackedEntity.Entity);
trackedEntity.Entity.UpdatedAt = DateTime.UtcNow;
trackedEntity.State = EntityState.Modified;
}
else
{
// Entity is not tracked, use Update method which handles state properly
_context.AgentSummaries.Update(entityToUpdate);
}
await _context.SaveChangesAsync();
_logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
}
await transaction.CommitAsync();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Error saving/updating AgentSummary for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
throw;
}
});
}
public async Task<(IEnumerable<AgentSummary> Results, int TotalCount)> GetPaginatedAsync(
int page,
int pageSize,
SortableFields sortBy,
string sortOrder,
IEnumerable<string>? agentNames = null,
bool showOnlyProfitableAgent = false)
{
// Start with base query
var query = _context.AgentSummaries.Include(a => a.User).AsQueryable();
// Apply agent name filtering if specified
if (agentNames != null && agentNames.Any())
{
query = query.Where(a => agentNames.Contains(a.AgentName));
}
// Apply profitable agent filtering if specified
if (showOnlyProfitableAgent)
{
query = query.Where(a => a.TotalROI > 0);
}
// Get total count before applying pagination
var totalCount = await query.CountAsync();
// Apply sorting
var isDescending = sortOrder.ToLowerInvariant() == "desc";
query = sortBy switch
{
SortableFields.NetPnL => isDescending
? query.OrderByDescending(a => a.NetPnL)
: query.OrderBy(a => a.NetPnL),
SortableFields.TotalROI => isDescending
? query.OrderByDescending(a => a.TotalROI)
: query.OrderBy(a => a.TotalROI),
SortableFields.Wins => isDescending
? query.OrderByDescending(a => a.Wins)
: query.OrderBy(a => a.Wins),
SortableFields.Losses => isDescending
? query.OrderByDescending(a => a.Losses)
: query.OrderBy(a => a.Losses),
SortableFields.AgentName => isDescending
? query.OrderByDescending(a => a.AgentName)
: query.OrderBy(a => a.AgentName),
SortableFields.CreatedAt => isDescending
? query.OrderByDescending(a => a.CreatedAt)
: query.OrderBy(a => a.CreatedAt),
SortableFields.UpdatedAt => isDescending
? query.OrderByDescending(a => a.UpdatedAt)
: query.OrderBy(a => a.UpdatedAt),
SortableFields.TotalVolume => isDescending
? query.OrderByDescending(a => a.TotalVolume)
: query.OrderBy(a => a.TotalVolume),
SortableFields.TotalBalance => isDescending
? query.OrderByDescending(a => a.TotalBalance)
: query.OrderBy(a => a.TotalBalance),
_ => isDescending
? query.OrderByDescending(a => a.TotalPnL) // Default to TotalPnL desc
: query.OrderBy(a => a.TotalPnL)
};
// Apply pagination
var results = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// Map to domain objects
var domainResults = results.Select(MapToDomain);
return (domainResults, totalCount);
}
private static AgentSummaryEntity MapToEntity(AgentSummary domain)
{
return new AgentSummaryEntity
{
Id = domain.Id,
UserId = domain.UserId,
AgentName = domain.AgentName,
TotalPnL = domain.TotalPnL,
NetPnL = domain.NetPnL,
TotalROI = domain.TotalROI,
Wins = domain.Wins,
Losses = domain.Losses,
Runtime = domain.Runtime,
CreatedAt = domain.CreatedAt,
UpdatedAt = domain.UpdatedAt,
ActiveStrategiesCount = domain.ActiveStrategiesCount,
TotalVolume = domain.TotalVolume,
TotalBalance = domain.TotalBalance,
TotalFees = domain.TotalFees,
BacktestCount = domain.BacktestCount
};
}
private static void MapToEntity(AgentSummary domain, AgentSummaryEntity entity)
{
entity.UserId = domain.UserId;
entity.AgentName = domain.AgentName;
entity.TotalPnL = domain.TotalPnL;
entity.NetPnL = domain.NetPnL;
entity.TotalROI = domain.TotalROI;
entity.Wins = domain.Wins;
entity.Losses = domain.Losses;
entity.Runtime = domain.Runtime;
entity.ActiveStrategiesCount = domain.ActiveStrategiesCount;
entity.TotalVolume = domain.TotalVolume;
entity.TotalBalance = domain.TotalBalance;
entity.TotalFees = domain.TotalFees;
// BacktestCount is NOT updated here - it's managed independently via IncrementBacktestCountAsync
// This prevents other update operations from overwriting the BacktestCount
}
private static AgentSummary MapToDomain(AgentSummaryEntity entity)
{
return new AgentSummary
{
Id = entity.Id,
UserId = entity.UserId,
AgentName = entity.AgentName,
TotalPnL = entity.TotalPnL,
NetPnL = entity.NetPnL,
TotalROI = entity.TotalROI,
Wins = entity.Wins,
Losses = entity.Losses,
Runtime = entity.Runtime,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
ActiveStrategiesCount = entity.ActiveStrategiesCount,
TotalVolume = entity.TotalVolume,
TotalBalance = entity.TotalBalance,
TotalFees = entity.TotalFees,
BacktestCount = entity.BacktestCount,
User = PostgreSqlMappers.Map(entity.User)
};
}
public async Task<IEnumerable<AgentSummary>> GetAllAgentWithRunningBots()
{
var agentSummaries = await _context.AgentSummaries
.Include(a => a.User)
.Where(a => _context.Bots.Any(b => b.UserId == a.UserId && b.Status == BotStatus.Running))
.ToListAsync();
return agentSummaries.Select(MapToDomain);
}
public async Task UpdateAgentNameAsync(int userId, string agentName)
{
try
{
// First, check if there's already a tracked entity with this key
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
.FirstOrDefault(e => e.Entity.UserId == userId);
AgentSummaryEntity? entityToUpdate = null;
bool wasTracked = false;
if (trackedEntity != null)
{
// Entity is already tracked, update it directly
entityToUpdate = trackedEntity.Entity;
wasTracked = true;
_logger.LogInformation("Found tracked entity for user {UserId}. Current agent name: {CurrentAgentName}, New agent name: {NewAgentName}",
userId, entityToUpdate.AgentName, agentName);
}
else
{
// Entity is not tracked, fetch it normally
entityToUpdate = await _context.AgentSummaries
.FirstOrDefaultAsync(a => a.UserId == userId);
if (entityToUpdate == null)
{
_logger.LogWarning(
"No AgentSummary found for user {UserId} when trying to update agent name to {AgentName}",
userId, agentName);
return;
}
_logger.LogInformation("Fetched entity for user {UserId}. Current agent name: {CurrentAgentName}, New agent name: {NewAgentName}",
userId, entityToUpdate.AgentName, agentName);
}
// Check if the agent name is actually different
if (entityToUpdate.AgentName == agentName)
{
_logger.LogInformation("Agent name for user {UserId} is already {AgentName}, no update needed", userId, agentName);
return;
}
// Update the entity
var oldAgentName = entityToUpdate.AgentName;
entityToUpdate.AgentName = agentName;
entityToUpdate.UpdatedAt = DateTime.UtcNow;
// If it wasn't tracked, explicitly mark it as modified
if (!wasTracked)
{
_context.Entry(entityToUpdate).State = EntityState.Modified;
}
// Log the change tracker state before saving
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.ToList();
_logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count);
// Save changes
var changesSaved = await _context.SaveChangesAsync();
_logger.LogInformation("Agent name updated for user {UserId} from '{OldAgentName}' to '{NewAgentName}'. Changes saved: {ChangesSaved}",
userId, oldAgentName, agentName, changesSaved);
if (changesSaved == 0)
{
_logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating agent name for user {UserId} to {AgentName}", userId, agentName);
throw;
}
}
public async Task<int> GetTotalAgentCount()
{
return await _context.AgentSummaries.CountAsync();
}
public async Task IncrementBacktestCountAsync(int userId)
{
try
{
// First, check if there's already a tracked entity with this key
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
.FirstOrDefault(e => e.Entity.UserId == userId);
AgentSummaryEntity? entityToUpdate = null;
bool wasTracked = false;
if (trackedEntity != null)
{
// Entity is already tracked, update it directly
entityToUpdate = trackedEntity.Entity;
wasTracked = true;
_logger.LogInformation("Found tracked entity for user {UserId}. Current backtest count: {CurrentCount}",
userId, entityToUpdate.BacktestCount);
}
else
{
// Entity is not tracked, fetch it normally
entityToUpdate = await _context.AgentSummaries
.FirstOrDefaultAsync(a => a.UserId == userId);
if (entityToUpdate == null)
{
_logger.LogWarning("No AgentSummary found for user {UserId} when trying to increment backtest count",
userId);
return;
}
_logger.LogInformation("Fetched entity for user {UserId}. Current backtest count: {CurrentCount}",
userId, entityToUpdate.BacktestCount);
}
// Update the entity
var oldCount = entityToUpdate.BacktestCount;
var newCount = oldCount + 1;
entityToUpdate.BacktestCount = newCount;
entityToUpdate.UpdatedAt = DateTime.UtcNow;
// If it wasn't tracked, explicitly mark it as modified
if (!wasTracked)
{
_context.Entry(entityToUpdate).State = EntityState.Modified;
}
// Log the change tracker state before saving
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.ToList();
_logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count);
// Save changes
var changesSaved = await _context.SaveChangesAsync();
_logger.LogInformation("Backtest count incremented for user {UserId} from {OldCount} to {NewCount}. Changes saved: {ChangesSaved}",
userId, oldCount, newCount, changesSaved);
if (changesSaved == 0)
{
_logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error incrementing backtest count for user {UserId}", userId);
throw;
}
}
public async Task UpdateTotalBalanceAsync(int userId, decimal totalBalance)
{
try
{
// First, check if there's already a tracked entity with this key
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
.FirstOrDefault(e => e.Entity.UserId == userId);
AgentSummaryEntity? entityToUpdate = null;
bool wasTracked = false;
if (trackedEntity != null)
{
// Entity is already tracked, update it directly
entityToUpdate = trackedEntity.Entity;
wasTracked = true;
_logger.LogInformation("Found tracked entity for user {UserId}. Current total balance: {CurrentBalance}, New total balance: {NewBalance}",
userId, entityToUpdate.TotalBalance, totalBalance);
}
else
{
// Entity is not tracked, fetch it normally
entityToUpdate = await _context.AgentSummaries
.FirstOrDefaultAsync(a => a.UserId == userId);
if (entityToUpdate == null)
{
_logger.LogWarning("No AgentSummary found for user {UserId} when trying to update total balance to {TotalBalance}",
userId, totalBalance);
return;
}
_logger.LogInformation("Fetched entity for user {UserId}. Current total balance: {CurrentBalance}, New total balance: {NewBalance}",
userId, entityToUpdate.TotalBalance, totalBalance);
}
// Check if the total balance is actually different
if (entityToUpdate.TotalBalance == totalBalance)
{
_logger.LogInformation("Total balance for user {UserId} is already {TotalBalance}, no update needed", userId, totalBalance);
return;
}
// Update the entity
var oldBalance = entityToUpdate.TotalBalance;
entityToUpdate.TotalBalance = totalBalance;
entityToUpdate.UpdatedAt = DateTime.UtcNow;
// If it wasn't tracked, explicitly mark it as modified
if (!wasTracked)
{
_context.Entry(entityToUpdate).State = EntityState.Modified;
}
// Log the change tracker state before saving
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.ToList();
_logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count);
// Save changes
var changesSaved = await _context.SaveChangesAsync();
_logger.LogInformation("Total balance updated for user {UserId} from {OldBalance} to {NewBalance}. Changes saved: {ChangesSaved}",
userId, oldBalance, totalBalance, changesSaved);
if (changesSaved == 0)
{
_logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating total balance for user {UserId} to {TotalBalance}", userId, totalBalance);
throw;
}
}
}