diff --git a/Directory.Build.props b/Directory.Build.props index e8af0ce..ba4b6de 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,8 @@ latest strict $(NoWarn);1591 + + false diff --git a/League/League.csproj b/League/League.csproj index 8ffd7d6..cd3e59d 100644 --- a/League/League.csproj +++ b/League/League.csproj @@ -20,8 +20,6 @@ Localizations for English and German are included. The library is in operation o true false true - - false en;de enable diff --git a/TournamentManager/DAL/Code/Markdown/EntityModel/_DefaultGroup/Entities/Tournament.md b/TournamentManager/DAL/Code/Markdown/EntityModel/_DefaultGroup/Entities/Tournament.md index 5bc47c2..7643124 100644 --- a/TournamentManager/DAL/Code/Markdown/EntityModel/_DefaultGroup/Entities/Tournament.md +++ b/TournamentManager/DAL/Code/Markdown/EntityModel/_DefaultGroup/Entities/Tournament.md @@ -17,7 +17,7 @@ Related Entity | Full description [Ranking](../../_DefaultGroup/Entities/Ranking.htm) | Ranking.Tournament - Tournament.Rankings (m:1) [Registration](../../_DefaultGroup/Entities/Registration.htm) | Registration.Tournament - Tournament.Registrations (m:1) [Round](../../_DefaultGroup/Entities/Round.htm) | Round.Tournament - Tournament.Rounds (m:1) -[Tournament](../../_DefaultGroup/Entities/Tournament.htm) | Tournament.Tournament - Tournament.Tournaments (m:1) +[Tournament](../../_DefaultGroup/Entities/Tournament.htm) | Tournament.NextTournament - Tournament.Tournaments (m:1) [TournamentType](../../_DefaultGroup/Entities/TournamentType.htm) | Tournament.TournamentType - TournamentType.Tournaments (m:1) ## Fields @@ -139,22 +139,22 @@ Setting name | Value --|-- Navigator property is public | True -#### Rankings (NavigatorCollection) +#### NextTournament (NavigatorSingleValue) Setting name | Value --|-- Navigator property is public | True -#### Registrations (NavigatorCollection) +#### Rankings (NavigatorCollection) Setting name | Value --|-- Navigator property is public | True -#### Rounds (NavigatorCollection) +#### Registrations (NavigatorCollection) Setting name | Value --|-- Navigator property is public | True -#### Tournament (NavigatorSingleValue) +#### Rounds (NavigatorCollection) Setting name | Value --|-- Navigator property is public | True @@ -171,7 +171,7 @@ Navigator property is public | True ### Attribute definitions per element -#### Tournament (NavigatorSingleValue) +#### NextTournament (NavigatorSingleValue) * `Browsable($false)` diff --git a/TournamentManager/DAL/Code/Markdown/generalinformation.md b/TournamentManager/DAL/Code/Markdown/generalinformation.md index 93d6376..741d530 100644 --- a/TournamentManager/DAL/Code/Markdown/generalinformation.md +++ b/TournamentManager/DAL/Code/Markdown/generalinformation.md @@ -4,7 +4,7 @@ This information is generated from the project `TournamentManager`, using the file `D:\Internet\Websites\League\Src\TournamentManager\DAL\TournamentManager.llblgenproj`. --|-- -Generated on | 20-Apr-2024 +Generated on | 31-Mai-2024 Using LLBLGen Pro version | LLBLGen Pro v5.11 RTM (v5.11.1) Project creator | axuno gGmbH Project name | TournamentManager diff --git a/TournamentManager/DAL/Code/Markdown/index.md b/TournamentManager/DAL/Code/Markdown/index.md index 41c2014..e6ec67e 100644 --- a/TournamentManager/DAL/Code/Markdown/index.md +++ b/TournamentManager/DAL/Code/Markdown/index.md @@ -2,6 +2,6 @@

TournamentManager Model Documentation

Generated by LLBLGen Pro v5.11
-Generated on 20-Apr-2024
+Generated on 31-Mai-2024









\ No newline at end of file diff --git a/TournamentManager/DAL/DatabaseGeneric/EntityClasses/TournamentEntity.cs b/TournamentManager/DAL/DatabaseGeneric/EntityClasses/TournamentEntity.cs index 8c2060d..0b50ca6 100644 --- a/TournamentManager/DAL/DatabaseGeneric/EntityClasses/TournamentEntity.cs +++ b/TournamentManager/DAL/DatabaseGeneric/EntityClasses/TournamentEntity.cs @@ -31,7 +31,7 @@ public partial class TournamentEntity : CommonEntityBase private EntityCollection _registrations; private EntityCollection _rounds; private EntityCollection _tournaments; - private TournamentEntity _tournament; + private TournamentEntity _nextTournament; private TournamentTypeEntity _tournamentType; // __LLBLGENPRO_USER_CODE_REGION_START PrivateMembers @@ -42,8 +42,8 @@ public partial class TournamentEntity : CommonEntityBase /// All names of fields mapped onto a relation. Usable for in-memory filtering public static partial class MemberNames { - /// Member name Tournament - public static readonly string Tournament = "Tournament"; + /// Member name NextTournament + public static readonly string NextTournament = "NextTournament"; /// Member name TournamentType public static readonly string TournamentType = "TournamentType"; /// Member name ExcludeMatchDates @@ -69,7 +69,7 @@ public TournamentEntityStaticMetaData() AddNavigatorMetaData>("Registrations", a => a._registrations, (a, b) => a._registrations = b, a => a.Registrations, () => new TournamentRelations().RegistrationEntityUsingTournamentId, typeof(RegistrationEntity), (int)TournamentManager.DAL.EntityType.RegistrationEntity); AddNavigatorMetaData>("Rounds", a => a._rounds, (a, b) => a._rounds = b, a => a.Rounds, () => new TournamentRelations().RoundEntityUsingTournamentId, typeof(RoundEntity), (int)TournamentManager.DAL.EntityType.RoundEntity); AddNavigatorMetaData>("Tournaments", a => a._tournaments, (a, b) => a._tournaments = b, a => a.Tournaments, () => new TournamentRelations().TournamentEntityUsingNextTournamentId, typeof(TournamentEntity), (int)TournamentManager.DAL.EntityType.TournamentEntity); - AddNavigatorMetaData("Tournament", "Tournaments", (a, b) => a._tournament = b, a => a._tournament, (a, b) => a.Tournament = b, TournamentManager.DAL.RelationClasses.StaticTournamentRelations.TournamentEntityUsingIdNextTournamentIdStatic, ()=>new TournamentRelations().TournamentEntityUsingIdNextTournamentId, null, new int[] { (int)TournamentFieldIndex.NextTournamentId }, null, true, (int)TournamentManager.DAL.EntityType.TournamentEntity); + AddNavigatorMetaData("NextTournament", "Tournaments", (a, b) => a._nextTournament = b, a => a._nextTournament, (a, b) => a.NextTournament = b, TournamentManager.DAL.RelationClasses.StaticTournamentRelations.TournamentEntityUsingIdNextTournamentIdStatic, ()=>new TournamentRelations().TournamentEntityUsingIdNextTournamentId, null, new int[] { (int)TournamentFieldIndex.NextTournamentId }, null, true, (int)TournamentManager.DAL.EntityType.TournamentEntity); AddNavigatorMetaData("TournamentType", "Tournaments", (a, b) => a._tournamentType = b, a => a._tournamentType, (a, b) => a.TournamentType = b, TournamentManager.DAL.RelationClasses.StaticTournamentRelations.TournamentTypeEntityUsingTypeIdStatic, ()=>new TournamentRelations().TournamentTypeEntityUsingTypeId, null, new int[] { (int)TournamentFieldIndex.TypeId }, null, true, (int)TournamentManager.DAL.EntityType.TournamentTypeEntity); } } @@ -145,7 +145,7 @@ protected TournamentEntity(SerializationInfo info, StreamingContext context) : b /// Creates a new IRelationPredicateBucket object which contains the predicate expression and relation collection to fetch the related entity of type 'Tournament' to this entity. /// - public virtual IRelationPredicateBucket GetRelationInfoTournament() { return CreateRelationInfoForNavigator("Tournament"); } + public virtual IRelationPredicateBucket GetRelationInfoNextTournament() { return CreateRelationInfoForNavigator("NextTournament"); } /// Creates a new IRelationPredicateBucket object which contains the predicate expression and relation collection to fetch the related entity of type 'TournamentType' to this entity. /// @@ -203,7 +203,7 @@ private void InitClassEmpty(IValidator validator, IEntityFields2 fields) /// Creates a new PrefetchPathElement2 object which contains all the information to prefetch the related entities of type 'Tournament' for this entity. /// Ready to use IPrefetchPathElement2 implementation. - public static IPrefetchPathElement2 PrefetchPathTournament { get { return _staticMetaData.GetPrefetchPathElement("Tournament", CommonEntityBase.CreateEntityCollection()); } } + public static IPrefetchPathElement2 PrefetchPathNextTournament { get { return _staticMetaData.GetPrefetchPathElement("NextTournament", CommonEntityBase.CreateEntityCollection()); } } /// Creates a new PrefetchPathElement2 object which contains all the information to prefetch the related entities of type 'TournamentType' for this entity. /// Ready to use IPrefetchPathElement2 implementation. @@ -299,14 +299,14 @@ public virtual System.DateTime ModifiedOn /// Gets the EntityCollection with the related entities of type 'TournamentEntity' which are related to this entity via a relation of type '1:n'. If the EntityCollection hasn't been fetched yet, the collection returned will be empty.

[TypeContainedAttribute(typeof(TournamentEntity))] - public virtual EntityCollection Tournaments { get { return GetOrCreateEntityCollection("Tournament", true, false, ref _tournaments); } } + public virtual EntityCollection Tournaments { get { return GetOrCreateEntityCollection("NextTournament", true, false, ref _tournaments); } } /// Gets / sets related entity of type 'TournamentEntity' which has to be set using a fetch action earlier. If no related entity is set for this property, null is returned..

[Browsable(false)] - public virtual TournamentEntity Tournament + public virtual TournamentEntity NextTournament { - get { return _tournament; } - set { SetSingleRelatedEntityNavigator(value, "Tournament"); } + get { return _nextTournament; } + set { SetSingleRelatedEntityNavigator(value, "NextTournament"); } } /// Gets / sets related entity of type 'TournamentTypeEntity' which has to be set using a fetch action earlier. If no related entity is set for this property, null is returned..

@@ -388,7 +388,7 @@ public virtual IEntityRelation TournamentEntityUsingNextTournamentId /// Returns a new IEntityRelation object, between TournamentEntity and TournamentEntity over the m:1 relation they have, using the relation between the fields: Tournament.NextTournamentId - Tournament.Id public virtual IEntityRelation TournamentEntityUsingIdNextTournamentId { - get { return ModelInfoProviderSingleton.GetInstance().CreateRelation(RelationType.ManyToOne, "Tournament", false, new[] { TournamentFields.Id, TournamentFields.NextTournamentId }); } + get { return ModelInfoProviderSingleton.GetInstance().CreateRelation(RelationType.ManyToOne, "NextTournament", false, new[] { TournamentFields.Id, TournamentFields.NextTournamentId }); } } /// Returns a new IEntityRelation object, between TournamentEntity and TournamentTypeEntity over the m:1 relation they have, using the relation between the fields: Tournament.TypeId - TournamentType.Id diff --git a/TournamentManager/DAL/TournamentManager.llblgenproj b/TournamentManager/DAL/TournamentManager.llblgenproj index e060825..4842e41 100644 --- a/TournamentManager/DAL/TournamentManager.llblgenproj +++ b/TournamentManager/DAL/TournamentManager.llblgenproj @@ -743,7 +743,7 @@ - + diff --git a/TournamentManager/TournamentManager/Data/TournamentRepository.cs b/TournamentManager/TournamentManager/Data/TournamentRepository.cs index 1121398..617c66c 100644 --- a/TournamentManager/TournamentManager/Data/TournamentRepository.cs +++ b/TournamentManager/TournamentManager/Data/TournamentRepository.cs @@ -1,3 +1,4 @@ +using System.Data; using Microsoft.Extensions.Logging; using SD.LLBLGen.Pro.LinqSupportClasses; using SD.LLBLGen.Pro.ORMSupportClasses; @@ -52,7 +53,6 @@ public TournamentRepository(MultiTenancy.IDbContext dbContext) public virtual async Task> GetTournamentRoundsAsync(long tournamentId, CancellationToken cancellationToken) { using var da = _dbContext.GetNewAdapter(); - //var selectedRounds = new EntityCollection(); var metaData = new LinqMetaData(da); var q = await (from r in metaData.Round @@ -63,6 +63,18 @@ public virtual async Task> GetTournamentRoundsAsyn return result; } + public virtual async Task> GetTournamentRoundIdsAsync(long tournamentId, CancellationToken cancellationToken) + { + using var da = _dbContext.GetNewAdapter(); + var metaData = new LinqMetaData(da); + + var result = await (from r in metaData.Round + where r.TournamentId == tournamentId + select r.Id).ToListAsync(cancellationToken); + + return result; + } + public virtual async Task GetTournamentEntityForMatchSchedulerAsync(long tournamentId, CancellationToken cancellationToken) { var bucket = new RelationPredicateBucket(TournamentFields.Id == tournamentId); @@ -87,4 +99,42 @@ public virtual async Task> GetTournamentRoundsAsyn await da.FetchEntityCollectionAsync(qp, cancellationToken); return t.FirstOrDefault(); } + + internal virtual async Task SaveTournamentsAsync(TournamentEntity sourceTournament, TournamentEntity targetTournament, CancellationToken cancellationToken) + { + using var da = _dbContext.GetNewAdapter(); + + try + { + await da.StartTransactionAsync(IsolationLevel.ReadCommitted, + string.Concat(nameof(SaveTournamentsAsync), Guid.NewGuid().ToString("N")), cancellationToken); + + await da.SaveEntityAsync(targetTournament, true, true, cancellationToken); + + if (sourceTournament.NextTournamentId is null) + { + sourceTournament.NextTournamentId = targetTournament.Id; + sourceTournament.ModifiedOn = targetTournament.ModifiedOn; + await da.SaveEntityAsync(sourceTournament, true, false, cancellationToken); + _logger.LogDebug("{Property} set to {NextTournamentId}", nameof(TournamentEntity.NextTournamentId), sourceTournament.NextTournamentId); + } + else + { + _logger.LogDebug("{Property} was already set to {NextTournamentId}", nameof(TournamentEntity.NextTournamentId), sourceTournament.NextTournamentId); + } + + await da.CommitAsync(cancellationToken); + return true; + } + catch (Exception e) + { + _logger.LogCritical(e, "Error saving transaction for tournament IDs {TargetId} and {SourceId}", + targetTournament.Id, sourceTournament.Id); + + if (da.IsTransactionInProgress) + da.Rollback(); + + return false; + } + } } diff --git a/TournamentManager/TournamentManager/TournamentCreator.cs b/TournamentManager/TournamentManager/TournamentCreator.cs index ae7fda2..48c34ca 100644 --- a/TournamentManager/TournamentManager/TournamentCreator.cs +++ b/TournamentManager/TournamentManager/TournamentCreator.cs @@ -2,183 +2,221 @@ using SD.LLBLGen.Pro.ORMSupportClasses; using TournamentManager.DAL.EntityClasses; using TournamentManager.DAL.HelperClasses; -using TournamentManager.Data; using TournamentManager.MultiTenancy; namespace TournamentManager; /// -/// The Copy class is used to copy an existing tournament -/// to a new one. Usually, the sequence is as follows: -/// 1. Copy the tournament e.g. from id 10 to 11: -/// Copy.Tournament(10, 11); -/// 2. Copy the rounds of a tournament: -/// Copy.Round(10, 11, null); -/// (Rounds not needed can be given in the list of 3rd parameter) -/// 3. Copy the teams of tournament -/// and assign the teams to the rounds created in step 2: -/// Copy.TeamsWithPersons(10, 11, null); -/// (Teams not needed can be given in the list of 3rd parameter) +/// The TournamentCreator class is responsible for copying an existing tournament to a new one. +/// It provides methods to copy the tournament, rounds, and round legs from the source tournament to the target tournament. /// public class TournamentCreator { - private readonly ILogger _logger = AppLogging.CreateLogger(); + /// + /// Arguments for the method. + /// + /// The ID of the source tournament. + /// Try to set the source tournament as completed. + /// The name for the target tournament. + /// The description for the target tournament. + /// The leg dates to use for the target tournament round legs. The list cover maximum number of legs across akk rounds. + /// The to use for CreatedOn / ModifiedOn of entities. + public record CopyTournamentArgs( + long SourceTournamentId, + bool SetSourceTournamentCompleted, + string TargetName, + string? TargetDescription, + IList<(DateTime Start, DateTime End)> TargetLegDates, + IList RoundsToExclude, + DateTime ModifiedOn); + + private readonly ILogger _logger; private readonly IAppDb _appDb; + private DateTime _modifiedOn; private static TournamentCreator? _instance; - private TournamentCreator(IAppDb appDb) + + private TournamentCreator(IAppDb appDb, ILogger logger) { _appDb = appDb; + _logger = logger; } - public static TournamentCreator Instance(IAppDb appDb) + /// + /// Returns the singleton instance of the class. + /// + /// + /// + /// The singleton instance of the class. + public static TournamentCreator Instance(IAppDb appDb, ILogger logger) { - _instance ??= new TournamentCreator(appDb); + _instance ??= new TournamentCreator(appDb, logger); return _instance; } /// - /// Copies the tournament basic data and the tournament leg data - /// from the source to a new target tournament. The new tournament id must - /// not exist. For start and end date of leg data 1 year is added. + /// Creates a new tournament from the source tournament. + /// + /// Definition of changes from source to new tournament. + /// + /// + /// var copyArgs = new TournamentCreator.CopyTournamentArgs(25, false, "Tournament 2024/25", null, + /// new List<(DateTime Start, DateTime End)> { + /// (new DateTime(2024, 9, 23), new DateTime(2025, 2, 1)), // 1st leg + /// (new DateTime(2025, 2, 3), new DateTime(2025, 5, 30)) // 2nd leg + /// }, Array.Empty<long>(), DateTime.UtcNow); + /// + /// var success = await TournamentCreator + /// .Instance(AppDb, AppLogging.CreateLogger<TournamentCreator>()) + /// .CreateNewFromSourceTournament(copyArgs, CancellationToken.None); + /// + /// , if the new tournament was created successfully. + public async Task CreateNewFromSourceTournament(CopyTournamentArgs copyArgs, CancellationToken cancellationToken) + { + if (copyArgs.SetSourceTournamentCompleted) await SetTournamentCompleted(copyArgs.SourceTournamentId, cancellationToken); + + var (sourceTournament, targetTournament) = await CopyTournament(copyArgs, cancellationToken); + + var success = await CopyRoundsWithLegsToTarget(copyArgs, targetTournament, cancellationToken); + + if (!success) return success; + + try + { + await _appDb.TournamentRepository.SaveTournamentsAsync(sourceTournament, targetTournament, cancellationToken); + } + catch (Exception e) + { + _logger.LogCritical(e, "Error saving Tournaments from {TournamentCreator}", nameof(TournamentCreator)); + return false; + } + + return true; + } + + + /// + /// Copies the tournament basic data from the source to a new target tournament . /// - /// Existing source tournament id. - /// - /// + /// The to be used. /// - /// True, if creation was successful, false otherwise. - public async Task CopyTournament (long fromTournamentId, string newName, string? newDescription, CancellationToken cancellationToken) + /// A with the Source (unchanged) and the new Target . + internal async Task<(TournamentEntity Source, TournamentEntity Target)> CopyTournament (CopyTournamentArgs copyArgs, CancellationToken cancellationToken) { - var now = DateTime.UtcNow; - var fromTournament = + _modifiedOn = copyArgs.ModifiedOn; + var sourceTournament = await _appDb.TournamentRepository.GetTournamentAsync( - new PredicateExpression(TournamentFields.Id == fromTournamentId), CancellationToken.None) - ?? throw new InvalidOperationException($"'{fromTournamentId}' not found."); + new PredicateExpression(TournamentFields.Id == copyArgs.SourceTournamentId), CancellationToken.None) + ?? throw new InvalidOperationException($"'{copyArgs.SourceTournamentId}' not found."); - var newTournament = new TournamentEntity + // Create the target tournament + var targetTournament = new TournamentEntity { IsPlanningMode = true, - Name = newName, - Description = newDescription, - TypeId = fromTournament.TypeId, + Name = copyArgs.TargetName, + Description = copyArgs.TargetDescription, + TypeId = sourceTournament.TypeId, IsComplete = false, - CreatedOn = now, - ModifiedOn = now, + CreatedOn = _modifiedOn, + ModifiedOn = _modifiedOn, }; - var success = await _appDb.GenericRepository.SaveEntityAsync(newTournament, true, false, cancellationToken); - _logger.LogInformation("New tournament {newTournament} saved successfully: {success}", newTournament, success); - if (!success) return null; - - fromTournament.NextTournamentId = newTournament.Id; - fromTournament.ModifiedOn = now; + // Update the source tournament + // sourceTournament.NextTournamentId (and sourceTournament.ModifiedOn) can be set immediately + // after the target tournament is saved, and NextTournamentId is NULL - // save last tournament - success = await _appDb.GenericRepository.SaveEntityAsync(fromTournament, true, false, cancellationToken); - _logger.LogInformation("Setting next tournament {newTournament} for {fromTournament} saved successfully: {success}", newTournament, fromTournament, success); - if (!success) return null; - - return newTournament.Id; + return (sourceTournament, targetTournament); } - /// /// Copies the round basic data and the round leg data - /// from the source to an existing target tournament. The new tournament id must - /// already exist. Leg data for each round is taken over from target tournament legs - /// on a 1:1 base (same number of legs, dates/times). + /// from the source to an existing target tournament. Leg basic data for each round is copied from the source to the target tournament. /// - /// Existing source tournament id. - /// Existing target tournament id. - /// List of round id's to be excluded (empty list for 'none') - /// The legs' start and end date. + /// + /// /// - /// True, if creation was successful, false otherwise. - public async Task CopyRoundWithLegs(long fromTournamentId, long toTournamentId, IList excludeRoundId, IList<(DateTime Start, DateTime End)> legDates, CancellationToken cancellationToken) + /// The with rounds and round legs added. + internal async Task CopyRoundsWithLegsToTarget(CopyTournamentArgs args, TournamentEntity targetTournament, CancellationToken cancellationToken) { - var transactionName = Guid.NewGuid().ToString("N"); - var now = DateTime.UtcNow; + // get the round IDs of the SOURCE tournament + var sourceRoundIds = await _appDb.TournamentRepository.GetTournamentRoundIdsAsync(args.SourceTournamentId, cancellationToken); - // get the rounds of SOURCE tournament - var roundIds = (await _appDb.TournamentRepository.GetTournamentRoundsAsync(fromTournamentId, cancellationToken)).Select(r => r.Id).ToList(); - - using var da = _appDb.DbContext.GetNewAdapter(); - - var roundsWithLegs = new Queue(); - foreach (var r in roundIds) + foreach (var sourceRoundId in sourceRoundIds) { - var round = await _appDb.RoundRepository.GetRoundWithLegsAsync(r, cancellationToken); - if (round != null) roundsWithLegs.Enqueue(round); - } + var sourceRound = await _appDb.RoundRepository.GetRoundWithLegsAsync(sourceRoundId, cancellationToken); - try - { - await da.StartTransactionAsync(System.Data.IsolationLevel.ReadUncommitted, transactionName, cancellationToken); + if (sourceRound is null) + { + _logger.LogCritical("Round {RoundId} not found.", sourceRoundId); + return false; + } - foreach (var r in roundIds) + // skip excluded round id's + if (args.RoundsToExclude.Contains(sourceRoundId)) { - var round = roundsWithLegs.Dequeue(); + _logger.LogDebug("Round {RoundId} excluded from copy.", sourceRoundId); + continue; + } - // skip excluded round id's - if (excludeRoundId.Contains(r)) - continue; + // Create target round from the source round + var targetRound = new RoundEntity + { + Tournament = targetTournament, // this adds the round to the target tournament + Name = sourceRound.Name, + Description = sourceRound.Description, + TypeId = sourceRound.TypeId, + NumOfLegs = sourceRound.NumOfLegs, + MatchRuleId = sourceRound.MatchRuleId, + SetRuleId = sourceRound.SetRuleId, + IsComplete = false, + CreatedOn = _modifiedOn, + ModifiedOn = _modifiedOn, + NextRoundId = null + }; + + var legDates = args.TargetLegDates; + + if (sourceRound.RoundLegs.Count > legDates.Count) + { + _logger.LogCritical("Round {RoundId} has {Legs} legs, but only {LegDates} leg dates provided.", sourceRoundId, sourceRound.RoundLegs.Count, legDates.Count); + return false; + } - // create new round and use data of source round - var newRound = new RoundEntity + // Create the round leg records based on the TARGET tournament legs, but use new log dates + for (var index = 0; index < sourceRound.RoundLegs.Count; index++) + { + var rl = sourceRound.RoundLegs[index]; + _ = new RoundLegEntity { - TournamentId = toTournamentId, - Name = round.Name, - Description = round.Description, - TypeId = round.TypeId, - NumOfLegs = round.NumOfLegs, - MatchRuleId = round.MatchRuleId, - SetRuleId = round.SetRuleId, - IsComplete = false, - CreatedOn = now, - ModifiedOn = now, - NextRoundId = null + Round = targetRound, // this adds the round leg to the target round + SequenceNo = rl.SequenceNo, + Description = rl.Description, + StartDateTime = legDates[index].Start, + EndDateTime = legDates[index].End, + CreatedOn = _modifiedOn, + ModifiedOn = _modifiedOn }; - - // create the round leg records based on the TARGET tournament legs, but use new log dates - for (var index = 0; index < round.RoundLegs.Count; index++) - { - var rl = round.RoundLegs[index]; - var newRoundLeg = new RoundLegEntity { - SequenceNo = rl.SequenceNo, - Description = rl.Description, - StartDateTime = index < legDates.Count ? legDates[index].Start : now, - EndDateTime = index < legDates.Count ? legDates[index].End : now, - CreatedOn = now, - ModifiedOn = now - }; - newRound.RoundLegs.Add(newRoundLeg); - } - - // save recursively (new round with the new round legs) - await da.SaveEntityAsync(newRound, true, true, cancellationToken); } - // commit after all rounds are processed successfully - await da.CommitAsync(cancellationToken); - _logger.LogInformation("{numOfRounds} rounds with legs saved successfully.", roundIds.Count); - return true; + _logger.LogDebug("Round {RoundId} with {Legs} legs copied to target tournament.", sourceRoundId, sourceRound.RoundLegs.Count); } - catch (Exception e) - { - _logger.LogCritical(e, "Error cloning rounds in transaction: New TournamentId={TournamentId}", toTournamentId); - if (da.IsTransactionInProgress) - da.Rollback(transactionName); + _logger.LogDebug("Target tournament contains {RoundCount} rounds.", targetTournament.Rounds.Count); - return false; - } + return true; } + /// + /// Sets the start and end dates for the legs having the of the specified . + /// + /// + /// + /// + /// + /// + /// , if successful. public async Task SetLegDates(ICollection rounds , int sequenceNo, DateTime start, DateTime end, CancellationToken cancellationToken) { - var transactionName = string.Concat(nameof(RankingRepository), nameof(SetLegDates), Guid.NewGuid().ToString("N")); - var now = DateTime.UtcNow; - if (rounds.Count == 0) return false; @@ -193,32 +231,27 @@ public async Task SetLegDates(ICollection rounds , int sequen if (!tournamentId.HasValue) return false; - using var da = _appDb.DbContext.GetNewAdapter(); - try { - await da.StartTransactionAsync(System.Data.IsolationLevel.ReadUncommitted, transactionName, cancellationToken); - foreach (var round in rounds) { foreach (var leg in round.RoundLegs.Where(l => l.SequenceNo == sequenceNo)) { leg.StartDateTime = start; leg.EndDateTime = end; - leg.ModifiedOn = now; + leg.ModifiedOn = _modifiedOn; - await da.SaveEntityAsync(leg, false, false, cancellationToken); + await _appDb.GenericRepository.SaveEntityAsync(leg, false, false, cancellationToken); + _logger.LogDebug("RoundLeg {RoundLegId} updated with new dates.", leg.Id); } } - await da.CommitAsync(cancellationToken); + return true; } catch (Exception e) { - _logger.LogCritical(e, "Error updating round legs in transaction: TournamentId={TournamentId}", tournamentId); + _logger.LogCritical(e, "Error updating round legs: TournamentId={TournamentId}", tournamentId); - if (da.IsTransactionInProgress) - da.Rollback(); return false; } } @@ -228,51 +261,70 @@ public async Task SetLegDates(ICollection rounds , int sequen /// /// The Tournament to be set as "completed" /// - /// Throws an exception if any match of the tournament is not completed yet. + /// Throws an exception if any match of the tournament is not completed yet. public async Task SetTournamentCompleted(long tournamentId, CancellationToken cancellationToken) { if (!await _appDb.MatchRepository.AllMatchesCompletedAsync(new TournamentEntity(tournamentId), cancellationToken)) { var ex = new InvalidOperationException($@"Tournament {tournamentId} contains incomplete matches."); - _logger.LogCritical(@"Tournament {TournamentId} contains incomplete matches. {Exception}", tournamentId, ex); + _logger.LogCritical(ex,@"Tournament {TournamentId} contains incomplete matches.", tournamentId); throw ex; } var tournament = await _appDb.TournamentRepository.GetTournamentWithRoundsAsync(tournamentId, CancellationToken.None) ?? throw new InvalidOperationException($"Tournament with Id '{tournamentId}' not found."); - var now = DateTime.UtcNow; + + // Note: Setting rounds and tournament as completed also removes inconsistencies + // (e.g. a tournament is marked as completed, but not all rounds are completed) foreach (var round in tournament.Rounds) { await SetRoundCompleted(round, cancellationToken); } - tournament.IsComplete = true; - tournament.ModifiedOn = now; - - using var da = _appDb.DbContext.GetNewAdapter(); - if (!await da.SaveEntityAsync(tournament, true, true, cancellationToken)) + if (!tournament.IsComplete) { - var ex = new InvalidOperationException($"Tournament Id {tournamentId} could not be saved to persistent storage."); - _logger.LogCritical(@"Tournament Id {TournamentId} could not be saved to persistent storage. {Exception}", tournamentId, ex); - throw ex; + tournament.IsComplete = true; + tournament.ModifiedOn = _modifiedOn; + await _appDb.GenericRepository.SaveEntityAsync(tournament, false, false, cancellationToken); + _logger.LogDebug("Tournament {Tournament} set as completed.", tournament); + } + else + { + _logger.LogDebug("Tournament {Tournament} was already set as completed.", tournament); } - _logger.LogInformation("Tournament {Tournament} set as completed.", tournament); } + /// + /// Sets the specified round as completed if all matches of the round are completed. + /// + /// The round to be set as completed. + /// + /// Thrown if any match of the round is not completed yet. public virtual async Task SetRoundCompleted(RoundEntity round, CancellationToken cancellationToken) { if (!await _appDb.MatchRepository.AllMatchesCompletedAsync(round, cancellationToken)) { var ex = new InvalidOperationException($"Round {round.Id} has uncompleted matches."); - _logger.LogCritical(@"Round {RoundId} has uncompleted matches. {Exception}", round.Id, ex); + _logger.LogCritical(ex,@"Round {RoundId} has uncompleted matches.", round.Id); throw ex; } - using var da = _appDb.DbContext.GetNewAdapter(); - da.FetchEntity(round); - round.IsComplete = true; - round.ModifiedOn = DateTime.UtcNow; - await da.SaveEntityAsync(round, cancellationToken); - _logger.LogInformation("Round {round} set as complete.", round); + if (round.IsDirty || round.IsNew) + { + round = (await _appDb.RoundRepository.GetRoundWithLegsAsync(round.Id, cancellationToken))!; + } + + if (!round.IsComplete) + { + round.IsComplete = true; + round.ModifiedOn = _modifiedOn; + + await _appDb.GenericRepository.SaveEntityAsync(round, false, false, cancellationToken); + _logger.LogDebug("Round {round} set as complete.", round); + } + else + { + _logger.LogDebug("Round {round} was already set as complete.", round); + } } }