Sube capa Infrastructure, Solution y archivos de configuracion del Backend
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for AuditLog entries, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The application's database context.</param>
|
||||
public class AuditLogRepository(ApplicationDBContext context)
|
||||
: GenericRepository<AuditLog>(context), IAuditLogRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for BlogPost entities, inheriting generic CRUD and implementing IBlogPostRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The application's database context used for data access operations.</param>
|
||||
public class BlogPostRepository(ApplicationDBContext context)
|
||||
: GenericRepository<BlogPost>(context), IBlogPostRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Club entities, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
public class ClubRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Club>(context), IClubRepository
|
||||
{ }
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Division entities, inheriting generic CRUD and implementing IDivisionRepository.
|
||||
/// </summary>
|
||||
public class DivisionRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Division>(context), IDivisionRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing DivisionTeamRegistration entities, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
public class DivisionTeamRegistrationRepository(ApplicationDBContext context)
|
||||
: GenericRepository<DivisionTeamRegistration>(context), IDivisionTeamRegistrationRepository
|
||||
{ }
|
||||
@@ -0,0 +1,143 @@
|
||||
using Application.DTOs.Abstract.Request;
|
||||
using Application.Interfaces.Repositories;
|
||||
using Application.Utils.Extensions;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a generic repository implementation for CRUD operations and querying entities in the database.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">
|
||||
/// The entity type managed by the repository. Must inherit from EntityBase.
|
||||
/// </typeparam>
|
||||
public class GenericRepository<TEntity>(ApplicationDBContext context)
|
||||
: IGenericRepository<TEntity> where TEntity : EntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The database context used for data access.
|
||||
/// </summary>
|
||||
protected readonly ApplicationDBContext _context = context;
|
||||
|
||||
/// <summary>
|
||||
/// The DbSet{TEntity} representing the collection of entities.
|
||||
/// </summary>
|
||||
protected readonly DbSet<TEntity> _dbSet = context.Set<TEntity>();
|
||||
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task UpdateRangeAsync(IEnumerable<TEntity> entities)
|
||||
{
|
||||
_dbSet.UpdateRange(entities);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task UpdateAsync(TEntity entity)
|
||||
{
|
||||
_dbSet.Update(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
///<inheritdoc />
|
||||
public virtual async Task<TEntity?> GetByIdAsync(Guid id, IEnumerable<Expression<Func<TEntity, object>>>? includes = null)
|
||||
{
|
||||
IQueryable<TEntity> query = _dbSet;
|
||||
|
||||
foreach (Expression<Func<TEntity, object>> include in includes ?? [])
|
||||
{
|
||||
query = query.Include(include);
|
||||
}
|
||||
|
||||
return await query.FirstOrDefaultAsync(e => e.Id == id);
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual IQueryable<TEntity> GetQueryable()
|
||||
{
|
||||
return _dbSet;
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<IEnumerable<TEntity>> GetAllAsync()
|
||||
{
|
||||
return await _dbSet.ToListAsync();
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<IEnumerable<TEntity>> FindAsync(
|
||||
Expression<Func<TEntity, bool>> predicate,
|
||||
IEnumerable<Expression<Func<TEntity, object>>>? includes = null,
|
||||
PaginatedFilterRequest? filter = null,
|
||||
bool asSplitQuery = false)
|
||||
{
|
||||
IQueryable<TEntity> query = _dbSet.Where(predicate);
|
||||
|
||||
foreach (Expression<Func<TEntity, object>> include in includes ?? [])
|
||||
{
|
||||
query = query.Include(include);
|
||||
}
|
||||
|
||||
if (asSplitQuery)
|
||||
{
|
||||
query = query.AsSplitQuery();
|
||||
}
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
query = query.SortBy(filter).Paginate(filter.PageNumber, filter.PageSize);
|
||||
}
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<TEntity> AddAsync(TEntity entity)
|
||||
{
|
||||
await _dbSet.AddAsync(entity);
|
||||
await _context.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<IEnumerable<TEntity>> AddRangeAsync(IEnumerable<TEntity> entities)
|
||||
{
|
||||
await _dbSet.AddRangeAsync(entities);
|
||||
await _context.SaveChangesAsync();
|
||||
return entities;
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual void Remove(TEntity entity)
|
||||
{
|
||||
_dbSet.Remove(entity);
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<int> RemoveAsync(Expression<Func<TEntity, bool>> expression)
|
||||
{
|
||||
return await _dbSet.Where(expression).ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null)
|
||||
{
|
||||
return predicate == null ? await _dbSet.CountAsync() : await _dbSet.CountAsync(predicate);
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
public virtual async Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate)
|
||||
{
|
||||
return await _dbSet.AnyAsync(predicate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Match entities, inheriting generic CRUD and implementing IMatchRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The application's database context used for data access.</param>
|
||||
public class MatchRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Match>(context), IMatchRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<Match?> GetDetailByIdOrSlugAsync(string idOrSlug)
|
||||
{
|
||||
IQueryable<Match> query = _context.Set<Match>()
|
||||
.Include(match => match.HomeTeam)
|
||||
.ThenInclude(team => team!.Players)
|
||||
.ThenInclude(player => player.PlayerTeamRegistrations)
|
||||
.Include(match => match.VisitorTeam)
|
||||
.ThenInclude(team => team!.Players)
|
||||
.ThenInclude(player => player.PlayerTeamRegistrations)
|
||||
.Include(match => match.Venue)
|
||||
// Stage → Division gives the match's tournament, used to pick each scorer's jersey number from that tournament's roster registration.
|
||||
.Include(match => match.Stage)
|
||||
.ThenInclude(stage => stage.Division)
|
||||
.Include(match => match.Scorers)
|
||||
.ThenInclude(scorer => scorer.Player)
|
||||
.ThenInclude(player => player!.PlayerTeamRegistrations)
|
||||
.AsSplitQuery();
|
||||
|
||||
return Guid.TryParse(idOrSlug, out Guid matchId)
|
||||
? await query.FirstOrDefaultAsync(match => match.Id == matchId)
|
||||
: await query.FirstOrDefaultAsync(match => match.Slug == idOrSlug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing MatchSeries entities, inheriting generic CRUD and implementing IMatchSeriesRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The application's database context used for data access.</param>
|
||||
public class MatchSeriesRepository(ApplicationDBContext context)
|
||||
: GenericRepository<MatchSeries>(context), IMatchSeriesRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Player entities, inheriting generic CRUD and implementing IPlayerRepository.
|
||||
/// </summary>
|
||||
public class PlayerRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Player>(context), IPlayerRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing PlayerSanction entities, inheriting generic CRUD and implementing IPlayerSanctionRepository.
|
||||
/// </summary>
|
||||
public class PlayerSanctionRepository(ApplicationDBContext context)
|
||||
: GenericRepository<PlayerSanction>(context), IPlayerSanctionRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing PlayerStatistic entities, inheriting generic CRUD and implementing IPlayerStatisticRepository.
|
||||
/// </summary>
|
||||
public class PlayerStatisticRepository(ApplicationDBContext context)
|
||||
: GenericRepository<PlayerStatistic>(context), IPlayerStatisticRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing PlayerTeamRegistration entities, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
public class PlayerTeamRegistrationRepository(ApplicationDBContext context)
|
||||
: GenericRepository<PlayerTeamRegistration>(context), IPlayerTeamRegistrationRepository
|
||||
{ }
|
||||
@@ -0,0 +1,144 @@
|
||||
using Application.DTOs.Scorer.Request;
|
||||
using Application.DTOs.Scorer.Response;
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for the top-scorers "goleadores" ranking, built from PlayerStatistic rows instead of the Scorer table.
|
||||
/// </summary>
|
||||
public class ScorerRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Scorer>(context), IScorerRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the top-scorers ranking by summing each player's Points-type PlayerStatistic rows instead of reading the Scorer table, keeping it in sync with the per-match loading path.
|
||||
/// </summary>
|
||||
public async Task<(IEnumerable<ScorerByPlayerResponse> Items, int TotalCount)> GetPlayerScoresAsync(GetScorerFilteredRequest filter)
|
||||
{
|
||||
IQueryable<Player> playersQuery = _context.Set<Player>();
|
||||
|
||||
if (filter.TournamentId.HasValue)
|
||||
{
|
||||
playersQuery = playersQuery.Where(p => p.Team.TournamentId == filter.TournamentId.Value);
|
||||
}
|
||||
|
||||
if (filter.MatchId.HasValue)
|
||||
{
|
||||
Guid matchId = filter.MatchId.Value;
|
||||
playersQuery = playersQuery.Where(p =>
|
||||
_context.Set<Match>().Any(m => m.Id == matchId &&
|
||||
(m.HomeTeamId == p.TeamId || m.VisitorTeamId == p.TeamId)));
|
||||
}
|
||||
|
||||
if (filter.TeamId.HasValue)
|
||||
{
|
||||
playersQuery = playersQuery.Where(p => p.TeamId == filter.TeamId.Value);
|
||||
}
|
||||
|
||||
if (filter.PlayerId.HasValue)
|
||||
{
|
||||
playersQuery = playersQuery.Where(p => p.Id == filter.PlayerId.Value);
|
||||
}
|
||||
|
||||
if (filter.DivisionId.HasValue)
|
||||
{
|
||||
Guid divisionId = filter.DivisionId.Value;
|
||||
playersQuery = playersQuery.Where(p =>
|
||||
_context.Set<StageTeamMatch>().Any(stm => stm.TeamId == p.TeamId && stm.Stage!.DivisionId == divisionId));
|
||||
}
|
||||
|
||||
if (filter.StageId.HasValue)
|
||||
{
|
||||
Guid stageId = filter.StageId.Value;
|
||||
playersQuery = playersQuery.Where(p =>
|
||||
_context.Set<StageTeamMatch>().Any(stm => stm.TeamId == p.TeamId && stm.StageId == stageId));
|
||||
}
|
||||
|
||||
// The goleadores ranking aggregates from PlayerStatistic, the same table the per-match loading path writes to, so there is no orphan Scorer write-gap and the ranking reflects real loaded points; only Points-type statistics count toward it.
|
||||
IQueryable<PlayerStatistic> scoreStatsQuery = _context.Set<PlayerStatistic>()
|
||||
.Where(s => s.Type == StatisticType.Points);
|
||||
|
||||
if (filter.TournamentId.HasValue)
|
||||
{
|
||||
scoreStatsQuery = scoreStatsQuery.Where(s => s.Match!.Stage.Division.TournamentId == filter.TournamentId.Value);
|
||||
}
|
||||
|
||||
if (filter.DivisionId.HasValue)
|
||||
{
|
||||
scoreStatsQuery = scoreStatsQuery.Where(s => s.Match!.Stage.DivisionId == filter.DivisionId.Value);
|
||||
}
|
||||
|
||||
if (filter.StageId.HasValue)
|
||||
{
|
||||
scoreStatsQuery = scoreStatsQuery.Where(s => s.Match!.StageId == filter.StageId.Value);
|
||||
}
|
||||
|
||||
if (filter.MatchId.HasValue)
|
||||
{
|
||||
scoreStatsQuery = scoreStatsQuery.Where(s => s.MatchId == filter.MatchId.Value);
|
||||
}
|
||||
|
||||
// A season is the calendar year of a tournament's StartDate, and grouping is safe by PlayerId because Player.DocumentNumber is uniquely indexed, so one real person is exactly one Player row reused across seasons rather than duplicated per season.
|
||||
if (filter.Season.HasValue)
|
||||
{
|
||||
int season = filter.Season.Value;
|
||||
|
||||
scoreStatsQuery = scoreStatsQuery.Where(s =>
|
||||
_context.Set<Tournament>().Any(t =>
|
||||
t.Id == s.Match!.Stage.Division.TournamentId && t.StartDate.Year == season));
|
||||
|
||||
playersQuery = playersQuery.Where(p => scoreStatsQuery.Any(s => s.PlayerId == p.Id));
|
||||
}
|
||||
|
||||
IQueryable<ScorerByPlayerResponse> query = playersQuery.Select(player => new ScorerByPlayerResponse
|
||||
{
|
||||
PlayerId = player.Id,
|
||||
FullName = (player.SecondName == null || player.SecondName == "")
|
||||
? player.LastName.ToUpper() + " " + player.FirstName
|
||||
: player.LastName.ToUpper() + " " + player.FirstName + " " + player.SecondName,
|
||||
Points = scoreStatsQuery
|
||||
.Where(s => s.PlayerId == player.Id)
|
||||
.Sum(s => (int?) s.Value) ?? 0,
|
||||
TeamId = player.TeamId,
|
||||
TeamName = player.Team.Name,
|
||||
TeamLogoUrl = player.Team.LogoUrl,
|
||||
TeamShirtColor = player.Team.ShirtColor,
|
||||
TeamJerseyStyle = player.Team.JerseyStyle,
|
||||
TeamShirtSecondaryColor = player.Team.ShirtSecondaryColor,
|
||||
TeamShirtTertiaryColor = player.Team.ShirtTertiaryColor,
|
||||
// The dorsal is season-scoped through PlayerTeamRegistration rather than a property of Player itself, so a tournament-scoped ranking uses that season's registration while a season or all-time aggregate spanning several registrations falls back to the player's most recent registration with their current team, since there is no single dorsal to show.
|
||||
JerseyNumber = filter.TournamentId.HasValue
|
||||
? _context.Set<PlayerTeamRegistration>()
|
||||
.Where(r => r.PlayerId == player.Id && r.TournamentId == filter.TournamentId.Value)
|
||||
.Select(r => r.JerseyNumber)
|
||||
.FirstOrDefault()
|
||||
: _context.Set<PlayerTeamRegistration>()
|
||||
.Where(r => r.PlayerId == player.Id && r.TeamId == player.TeamId)
|
||||
.OrderByDescending(r => r.DateCreated)
|
||||
.Select(r => r.JerseyNumber)
|
||||
.FirstOrDefault(),
|
||||
});
|
||||
|
||||
int totalCount = await query.CountAsync();
|
||||
|
||||
List<ScorerByPlayerResponse> items = await query
|
||||
.OrderByDescending(x => x.Points)
|
||||
.Skip((filter.PageNumber - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for Season entities, inheriting generic CRUD from GenericRepository and implementing ISeasonRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context used for data access operations.</param>
|
||||
public class SeasonRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Season>(context), ISeasonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Stage entities, inheriting generic CRUD from GenericRepository and implementing IStageRepository.
|
||||
/// </summary>
|
||||
public class StageRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Stage>(context), IStageRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing StageTeamMatch entities, inheriting generic CRUD from GenericRepository and implementing IStageTeamMatchRepository.
|
||||
/// </summary>
|
||||
public class StageTeamMatchRepository(ApplicationDBContext context)
|
||||
: GenericRepository<StageTeamMatch>(context), IStageTeamMatchRepository
|
||||
{ }
|
||||
@@ -0,0 +1,200 @@
|
||||
using Application.DTOs.Statistics.Response;
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
using Domain.Enums;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Historical-statistics aggregations that group each person's rows across seasons by their stable PlayerId, since a person is one Player row reused across seasons rather than duplicated per season.
|
||||
/// </summary>
|
||||
public class StatisticsRepository(ApplicationDBContext context) : IStatisticsRepository
|
||||
{
|
||||
private readonly ApplicationDBContext _context = context;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<PlayerStatisticCardResponse?> GetPlayerCardAsync(Guid playerId)
|
||||
{
|
||||
Player? player = await _context.Set<Player>()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||
|
||||
if (player is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<PointStatRow> rows = await _context.Set<PlayerStatistic>()
|
||||
.Where(s => s.PlayerId == playerId && s.Type == StatisticType.Points)
|
||||
.Select(s => new PointStatRow(s.MatchId, s.Value, s.Match!.Stage.Division.TournamentId))
|
||||
.ToListAsync();
|
||||
|
||||
Dictionary<Guid, int> seasonByTournament = await BuildSeasonByTournamentAsync(rows.Select(r => r.TournamentId));
|
||||
|
||||
List<SeasonStatLineResponse> seasons = [.. rows
|
||||
.GroupBy(r => seasonByTournament.TryGetValue(r.TournamentId, out int year) ? year : (int?)null)
|
||||
.Where(g => g.Key.HasValue)
|
||||
.Select(g => BuildSeasonLine(g.Key!.Value, g))
|
||||
.OrderByDescending(s => s.Season)];
|
||||
|
||||
int totalPoints = rows.Sum(r => r.Value);
|
||||
int gamesPlayed = rows.Select(r => r.MatchId).Distinct().Count();
|
||||
|
||||
return new PlayerStatisticCardResponse
|
||||
{
|
||||
PlayerId = player.Id,
|
||||
FullName = player.FullName,
|
||||
TotalPoints = totalPoints,
|
||||
GamesPlayed = gamesPlayed,
|
||||
AveragePoints = Average(totalPoints, gamesPlayed),
|
||||
Seasons = seasons,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<PlayerHistoryResponse?> GetPlayerHistoryAsync(Guid playerId)
|
||||
{
|
||||
Player? player = await _context.Set<Player>()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||
|
||||
if (player is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Season and team come from the season-scoped registrations, the source of truth for which team and which season, untouched by a team being reassigned to a later tournament.
|
||||
List<RegistrationRow> registrations = await _context.Set<PlayerTeamRegistration>()
|
||||
.Where(r => r.PlayerId == playerId)
|
||||
.Select(r => new RegistrationRow(
|
||||
r.TournamentId,
|
||||
r.Tournament!.Name,
|
||||
r.Tournament!.StartDate,
|
||||
r.TeamId,
|
||||
r.Team!.Name))
|
||||
.ToListAsync();
|
||||
|
||||
List<PointStatRow> statRows = await _context.Set<PlayerStatistic>()
|
||||
.Where(s => s.PlayerId == playerId && s.Type == StatisticType.Points)
|
||||
.Select(s => new PointStatRow(s.MatchId, s.Value, s.Match!.Stage.Division.TournamentId))
|
||||
.ToListAsync();
|
||||
|
||||
Dictionary<Guid, (int Points, int Games)> statsByTournament = statRows
|
||||
.GroupBy(r => r.TournamentId)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => (Points: g.Sum(x => x.Value), Games: g.Select(x => x.MatchId).Distinct().Count()));
|
||||
|
||||
List<SanctionRow> sanctionRows = await _context.Set<PlayerSanction>()
|
||||
.Where(ps => ps.PlayerId == playerId)
|
||||
.Select(ps => new SanctionRow(
|
||||
ps.Id,
|
||||
ps.Description,
|
||||
ps.Duration,
|
||||
ps.IssuedDate,
|
||||
ps.MatchId,
|
||||
ps.Match.Stage.Division.TournamentId))
|
||||
.ToListAsync();
|
||||
|
||||
Dictionary<Guid, List<PlayerHistorySanctionResponse>> sanctionsByTournament = sanctionRows
|
||||
.GroupBy(s => s.TournamentId)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Select(s => new PlayerHistorySanctionResponse
|
||||
{
|
||||
SanctionId = s.Id,
|
||||
Description = s.Description,
|
||||
Duration = s.Duration,
|
||||
IssuedDate = s.IssuedDate,
|
||||
MatchId = s.MatchId,
|
||||
}).ToList());
|
||||
|
||||
List<PlayerHistorySeasonResponse> seasons = [.. registrations
|
||||
.Select(r =>
|
||||
{
|
||||
(int points, int games) = statsByTournament.TryGetValue(r.TournamentId, out (int Points, int Games) stat)
|
||||
? stat
|
||||
: (0, 0);
|
||||
|
||||
IEnumerable<PlayerHistorySanctionResponse> sanctions =
|
||||
sanctionsByTournament.TryGetValue(r.TournamentId, out List<PlayerHistorySanctionResponse>? list)
|
||||
? list
|
||||
: [];
|
||||
|
||||
return new PlayerHistorySeasonResponse
|
||||
{
|
||||
Season = r.StartDate.Year,
|
||||
TournamentId = r.TournamentId,
|
||||
TournamentName = r.TournamentName,
|
||||
TeamId = r.TeamId,
|
||||
TeamName = r.TeamName,
|
||||
TotalPoints = points,
|
||||
GamesPlayed = games,
|
||||
Sanctions = sanctions,
|
||||
};
|
||||
})
|
||||
.OrderByDescending(s => s.Season)
|
||||
.ThenBy(s => s.TournamentName)];
|
||||
|
||||
return new PlayerHistoryResponse
|
||||
{
|
||||
PlayerId = player.Id,
|
||||
FullName = player.FullName,
|
||||
Seasons = seasons,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves each tournament id to its season, the year of its StartDate, in one query computed in memory so the season definition stays provider-independent.
|
||||
/// </summary>
|
||||
private async Task<Dictionary<Guid, int>> BuildSeasonByTournamentAsync(IEnumerable<Guid> tournamentIds)
|
||||
{
|
||||
List<Guid> ids = [.. tournamentIds.Distinct()];
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
List<TournamentSeasonRow> rows = await _context.Set<Tournament>()
|
||||
.Where(t => ids.Contains(t.Id))
|
||||
.Select(t => new TournamentSeasonRow(t.Id, t.StartDate))
|
||||
.ToListAsync();
|
||||
|
||||
return rows.ToDictionary(r => r.Id, r => r.StartDate.Year);
|
||||
}
|
||||
|
||||
private static SeasonStatLineResponse BuildSeasonLine(int season, IEnumerable<PointStatRow> rows)
|
||||
{
|
||||
int totalPoints = rows.Sum(r => r.Value);
|
||||
int gamesPlayed = rows.Select(r => r.MatchId).Distinct().Count();
|
||||
|
||||
return new SeasonStatLineResponse
|
||||
{
|
||||
Season = season,
|
||||
TotalPoints = totalPoints,
|
||||
GamesPlayed = gamesPlayed,
|
||||
AveragePoints = Average(totalPoints, gamesPlayed),
|
||||
};
|
||||
}
|
||||
|
||||
private static double Average(int totalPoints, int gamesPlayed) =>
|
||||
gamesPlayed == 0 ? 0 : Math.Round((double)totalPoints / gamesPlayed, 2);
|
||||
|
||||
private sealed record PointStatRow(Guid MatchId, int Value, Guid TournamentId);
|
||||
|
||||
private sealed record RegistrationRow(Guid TournamentId, string TournamentName, DateTime StartDate, Guid TeamId, string TeamName);
|
||||
|
||||
private sealed record SanctionRow(Guid Id, string Description, int Duration, DateTime IssuedDate, Guid MatchId, Guid TournamentId);
|
||||
|
||||
private sealed record TournamentSeasonRow(Guid Id, DateTime StartDate);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for TeamPointDeduction entities, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
public class TeamPointDeductionRepository(ApplicationDBContext context)
|
||||
: GenericRepository<TeamPointDeduction>(context), ITeamPointDeductionRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Team entities, inheriting generic CRUD from GenericRepository and implementing ITeamRepository.
|
||||
/// </summary>
|
||||
public class TeamRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Team>(context), ITeamRepository
|
||||
{ }
|
||||
@@ -0,0 +1,15 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for TeamStaff entities, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
public class TeamStaffRepository(ApplicationDBContext context)
|
||||
: GenericRepository<TeamStaff>(context), ITeamStaffRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing TeamTournamentRegistration entities, inheriting generic CRUD from GenericRepository.
|
||||
/// </summary>
|
||||
public class TeamTournamentRegistrationRepository(ApplicationDBContext context)
|
||||
: GenericRepository<TeamTournamentRegistration>(context), ITeamTournamentRegistrationRepository
|
||||
{ }
|
||||
@@ -0,0 +1,14 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for managing Tournament entities, inheriting generic CRUD from GenericRepository and implementing ITournamentRepository.
|
||||
/// </summary>
|
||||
public class TournamentRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Tournament>(context), ITournamentRepository
|
||||
{ }
|
||||
@@ -0,0 +1,147 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Implements IUnitOfWork, providing a centralized access point for all repository instances and managing transactional operations within the application's data layer.
|
||||
/// </summary>
|
||||
public class UnitOfWork(
|
||||
ApplicationDBContext context,
|
||||
IPlayerRepository playerRepository,
|
||||
IClubRepository clubRepository,
|
||||
ITeamRepository teamRepository,
|
||||
IDivisionRepository divisionRepository,
|
||||
IMatchRepository matchRepository,
|
||||
IMatchSeriesRepository matchSeriesRepository,
|
||||
ITournamentRepository tournamentRepository,
|
||||
IBlogPostRepository blogPostRepository,
|
||||
IPlayerSanctionRepository playerSanctionRepository,
|
||||
IPlayerStatisticRepository playerStatisticRepository,
|
||||
IVenueRepository venueRepository,
|
||||
IStageRepository stageRepository,
|
||||
IStageTeamMatchRepository stageTeamMatchRepository,
|
||||
IPlayerTeamRegistrationRepository playerTeamRegistrationRepository,
|
||||
ITeamTournamentRegistrationRepository teamTournamentRegistrationRepository,
|
||||
IDivisionTeamRegistrationRepository divisionTeamRegistrationRepository
|
||||
) : IUnitOfWork
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the repository for blog post entities.
|
||||
/// </summary>
|
||||
public IBlogPostRepository BlogPostRepository { get; } = blogPostRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for club entities, whose identity stays stable across seasons.
|
||||
/// </summary>
|
||||
public IClubRepository ClubRepository { get; } = clubRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for team entities.
|
||||
/// </summary>
|
||||
public ITeamRepository TeamRepository { get; } = teamRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for venue entities.
|
||||
/// </summary>
|
||||
public IVenueRepository VenueRepository { get; } = venueRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for tournament entities.
|
||||
/// </summary>
|
||||
public ITournamentRepository TournamentRepository { get; } = tournamentRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for stage entities.
|
||||
/// </summary>
|
||||
public IStageRepository StageRepository { get; } = stageRepository;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for player statistic entities.
|
||||
/// </summary>
|
||||
public IPlayerStatisticRepository PlayerStatisticRepository { get; } = playerStatisticRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for player sanction entities.
|
||||
/// </summary>
|
||||
public IPlayerSanctionRepository PlayerSanctionRepository { get; } = playerSanctionRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for player entities.
|
||||
/// </summary>
|
||||
public IPlayerRepository PlayerRepository { get; } = playerRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for match entities.
|
||||
/// </summary>
|
||||
public IMatchRepository MatchRepository { get; } = matchRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for match series entities.
|
||||
/// </summary>
|
||||
public IMatchSeriesRepository MatchSeriesRepository { get; } = matchSeriesRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for division entities.
|
||||
/// </summary>
|
||||
public IDivisionRepository DivisionRepository { get; } = divisionRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for stage team match entities.
|
||||
/// </summary>
|
||||
public IStageTeamMatchRepository StageTeamMatchRepository { get; } = stageTeamMatchRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for player-team registration entities.
|
||||
/// </summary>
|
||||
public IPlayerTeamRegistrationRepository PlayerTeamRegistrationRepository { get; } = playerTeamRegistrationRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for team-tournament registration entities.
|
||||
/// </summary>
|
||||
public ITeamTournamentRegistrationRepository TeamTournamentRegistrationRepository { get; } = teamTournamentRegistrationRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository for division-team registration entities.
|
||||
/// </summary>
|
||||
public IDivisionTeamRegistrationRepository DivisionTeamRegistrationRepository { get; } = divisionTeamRegistrationRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Commits all tracked changes to the database asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public async Task<int> SaveChangesAsync()
|
||||
{
|
||||
return await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteInTransactionAsync(Func<Task> operation)
|
||||
{
|
||||
IExecutionStrategy strategy = context.Database.CreateExecutionStrategy();
|
||||
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
await operation();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Application.Interfaces.Repositories;
|
||||
|
||||
using Domain.Entities.Models;
|
||||
|
||||
using Infrastructure.Persistance;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for Venue entities, inheriting generic CRUD from GenericRepository and implementing IVenueRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context used for data access operations.</param>
|
||||
public class VenueRepository(ApplicationDBContext context)
|
||||
: GenericRepository<Venue>(context), IVenueRepository
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user