Files
managing-apps/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs

396 lines
16 KiB
C#

using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Abstractions.Shared;
using Managing.Domain.Accounts;
using Managing.Domain.Users;
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 PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserRepository
{
private readonly ICacheService _cacheService;
public PostgreSqlUserRepository(ManagingDbContext context, ILogger<SqlQueryLogger> logger,
SentrySqlMonitoringService sentryMonitoringService, ICacheService cacheService)
: base(context, logger, sentryMonitoringService)
{
_cacheService = cacheService;
}
public async Task<User?> GetUserByAgentNameAsync(string agentName)
{
return await ExecuteWithLoggingAsync(async () =>
{
// Check cache first for frequently accessed users
var cacheKey = $"user_agent_{agentName}";
var cachedUser = _cacheService.GetValue<User>(cacheKey);
if (cachedUser != null)
{
return cachedUser;
}
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
// Optimized query with explicit SELECT to avoid loading unnecessary data
var userEntity = await _context.Users
.AsNoTracking()
.Where(u => u.AgentName == agentName)
.Select(u => new UserEntity
{
Id = u.Id,
Name = u.Name,
AgentName = u.AgentName,
AvatarUrl = u.AvatarUrl,
TelegramChannel = u.TelegramChannel,
OwnerWalletAddress = u.OwnerWalletAddress
})
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (userEntity == null)
return null;
var user = PostgreSqlMappers.Map(userEntity);
// Cache user for 5 minutes since user data doesn't change frequently
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5));
return user;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetUserByAgentNameAsync), ("agentName", agentName));
}
public async Task<User?> GetUserByIdAsync(int userId)
{
return await ExecuteWithLoggingAsync(async () =>
{
// Check cache first for frequently accessed users
var cacheKey = $"user_id_{userId}";
var cachedUser = _cacheService.GetValue<User>(cacheKey);
if (cachedUser != null)
{
return cachedUser;
}
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
// Optimized query with explicit SELECT to avoid loading unnecessary data
var userEntity = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new UserEntity
{
Id = u.Id,
Name = u.Name,
AgentName = u.AgentName,
AvatarUrl = u.AvatarUrl,
TelegramChannel = u.TelegramChannel,
OwnerWalletAddress = u.OwnerWalletAddress
})
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (userEntity == null)
return null;
var user = PostgreSqlMappers.Map(userEntity);
// Cache user for 5 minutes since user data doesn't change frequently
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5));
return user;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetUserByIdAsync), ("userId", userId));
}
public async Task<User> GetUserByNameAsync(string name, bool fetchAccounts = false)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
User user;
if (fetchAccounts)
{
// Fetch user with accounts in a single query
var userEntity = await _context.Users
.AsNoTracking()
.Include(u => u.Accounts)
.Where(u => u.Name == name)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (userEntity == null)
throw new InvalidOperationException($"User with name '{name}' not found");
user = PostgreSqlMappers.Map(userEntity);
// Map accounts using the existing mapper
if (userEntity.Accounts != null)
{
user.Accounts = userEntity.Accounts.Select(PostgreSqlMappers.Map).ToList();
}
else
{
user.Accounts = new List<Account>();
}
}
else
{
// Optimized query with explicit SELECT to avoid loading unnecessary data
var userEntity = await _context.Users
.AsNoTracking()
.Where(u => u.Name == name)
.Select(u => new UserEntity
{
Id = u.Id,
Name = u.Name,
AgentName = u.AgentName,
AvatarUrl = u.AvatarUrl,
TelegramChannel = u.TelegramChannel
})
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (userEntity == null)
throw new InvalidOperationException($"User with name '{name}' not found");
user = PostgreSqlMappers.Map(userEntity);
user.Accounts = new List<Account>(); // Initialize empty list
}
return user;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetUserByNameAsync), ("name", name), ("fetchAccounts", fetchAccounts));
}
public async Task<IEnumerable<User>> GetAllUsersAsync()
{
return await ExecuteWithLoggingAsync(async () =>
{
// Check cache first for all users
var cacheKey = "all_users";
var cachedUsers = _cacheService.GetValue<List<User>>(cacheKey);
if (cachedUsers != null)
{
return cachedUsers;
}
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
// Optimized query with explicit SELECT to avoid loading unnecessary data
var userEntities = await _context.Users
.AsNoTracking()
.Select(u => new UserEntity
{
Id = u.Id,
Name = u.Name,
AgentName = u.AgentName,
AvatarUrl = u.AvatarUrl,
TelegramChannel = u.TelegramChannel,
OwnerWalletAddress = u.OwnerWalletAddress
})
.ToListAsync()
.ConfigureAwait(false);
var users = userEntities.Select(PostgreSqlMappers.Map).ToList();
// Cache all users for 10 minutes since this data changes infrequently
_cacheService.SaveValue(cacheKey, users, TimeSpan.FromMinutes(10));
return users;
}
finally
{
// Always ensure the connection is closed after the operation
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetAllUsersAsync));
}
public async Task SaveOrUpdateUserAsync(User user)
{
await ExecuteWithLoggingAsync(async () =>
{
try
{
var existingUser = await _context.Users
.AsTracking()
.FirstOrDefaultAsync(u => u.Name == user.Name)
.ConfigureAwait(false);
string? oldAgentName = null;
if (existingUser != null)
{
// Capture old AgentName before updating for cache invalidation
oldAgentName = existingUser.AgentName;
// Update existing user
existingUser.AgentName = user.AgentName;
existingUser.AvatarUrl = user.AvatarUrl;
existingUser.TelegramChannel = user.TelegramChannel;
existingUser.OwnerWalletAddress = user.OwnerWalletAddress;
existingUser.IsAdmin = user.IsAdmin;
_context.Users.Update(existingUser);
// Update the user object with the existing user's ID
user.Id = existingUser.Id;
}
else
{
// Insert new user
var userEntity = PostgreSqlMappers.Map(user);
_context.Users.Add(userEntity);
// Update the user object with the database-generated ID after save
await _context.SaveChangesAsync().ConfigureAwait(false);
user.Id = userEntity.Id;
// Cache the new user
var newUserNameCacheKey = $"user_name_{user.Name}";
var newUserAgentCacheKey = $"user_agent_{user.AgentName}";
_cacheService.SaveValue(newUserNameCacheKey, user, TimeSpan.FromMinutes(5));
if (!string.IsNullOrEmpty(user.AgentName))
{
_cacheService.SaveValue(newUserAgentCacheKey, user, TimeSpan.FromMinutes(5));
}
// Invalidate all users cache since we added a new user
_cacheService.RemoveValue("all_users");
return; // Exit early since we already saved
}
await _context.SaveChangesAsync().ConfigureAwait(false);
// Invalidate cache for updated user - handle both old and new AgentName
var nameCacheKey = $"user_name_{user.Name}";
var nameWithAccountsCacheKey = $"user_name_with_accounts_{user.Name}";
var idCacheKey = $"user_id_{user.Id}";
_cacheService.RemoveValue(nameCacheKey);
_cacheService.RemoveValue(nameWithAccountsCacheKey);
_cacheService.RemoveValue(idCacheKey);
// Invalidate old AgentName cache if it existed
if (!string.IsNullOrEmpty(oldAgentName))
{
var oldAgentCacheKey = $"user_agent_{oldAgentName}";
_cacheService.RemoveValue(oldAgentCacheKey);
}
// Invalidate new AgentName cache if it exists
if (!string.IsNullOrEmpty(user.AgentName))
{
var newAgentCacheKey = $"user_agent_{user.AgentName}";
_cacheService.RemoveValue(newAgentCacheKey);
}
// Invalidate all users cache since we updated a user
_cacheService.RemoveValue("all_users");
}
catch (Exception e)
{
Console.WriteLine(e);
throw new Exception("Cannot save or update user");
}
}, nameof(SaveOrUpdateUserAsync), ("userName", user.Name), ("userId", user.Id));
}
public async Task<(IEnumerable<User> Users, int TotalCount)> GetUsersPaginatedAsync(int page, int pageSize, UserSortableColumn sortBy, string sortOrder, UsersFilter filter)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var query = _context.Users.AsNoTracking();
// Apply filters
if (!string.IsNullOrWhiteSpace(filter.UserNameContains))
{
query = query.Where(u => EF.Functions.ILike(u.Name, $"%{filter.UserNameContains.Trim()}%"));
}
if (!string.IsNullOrWhiteSpace(filter.OwnerAddressContains))
{
query = query.Where(u => EF.Functions.ILike(u.OwnerWalletAddress, $"%{filter.OwnerAddressContains.Trim()}%"));
}
if (!string.IsNullOrWhiteSpace(filter.AgentNameContains))
{
query = query.Where(u => EF.Functions.ILike(u.AgentName, $"%{filter.AgentNameContains.Trim()}%"));
}
if (!string.IsNullOrWhiteSpace(filter.TelegramChannelContains))
{
query = query.Where(u => EF.Functions.ILike(u.TelegramChannel, $"%{filter.TelegramChannelContains.Trim()}%"));
}
// Get total count for pagination
var totalCount = await query.CountAsync().ConfigureAwait(false);
// Apply sorting
query = sortBy switch
{
UserSortableColumn.Id => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id),
UserSortableColumn.Name => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.Name) : query.OrderBy(u => u.Name),
UserSortableColumn.OwnerWalletAddress => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.OwnerWalletAddress) : query.OrderBy(u => u.OwnerWalletAddress),
UserSortableColumn.AgentName => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.AgentName) : query.OrderBy(u => u.AgentName),
_ => query.OrderBy(u => u.Id) // Default sorting
};
// Apply pagination
var users = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(u => new UserEntity
{
Id = u.Id,
Name = u.Name,
AgentName = u.AgentName,
AvatarUrl = u.AvatarUrl,
TelegramChannel = u.TelegramChannel,
OwnerWalletAddress = u.OwnerWalletAddress,
IsAdmin = u.IsAdmin,
LastConnectionDate = u.LastConnectionDate
})
.ToListAsync()
.ConfigureAwait(false);
var domainUsers = users.Select(PostgreSqlMappers.Map).ToList();
return (domainUsers, totalCount);
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetUsersPaginatedAsync), ("page", page), ("pageSize", pageSize), ("sortBy", sortBy), ("sortOrder", sortOrder));
}
}