protected internal async virtual Task <bool> TryUpdateIsLatestInDatabase(IEntitiesContext context) { // Use the EF change tracker to identify changes made in TryUpdateIsLatestAsync which // need to be applied to the database below. // Note that the change tracker is not mocked which make this method hard to unit test. var changeTracker = context.GetChangeTracker(); var modifiedPackages = changeTracker.Entries <Package>().Where(p => p.State == EntityState.Modified).ToList(); if (modifiedPackages.Count == 0) { return(true); } // Apply changes to the database with an optimistic concurrency check to prevent multiple // threads (in the same or different gallery instance) from setting IsLatest/IsLatestStable // flag to true on different package versions. // To preserve existing behavior, we only want to reject concurrent updates which set the // IsLatest/IsLatestStable columns. For this reason, we must avoid the EF ConcurrencyCheck // attribute which could reject any package update or delete. var query = new StringBuilder("DECLARE @rowCount INT = 0"); foreach (var packageEntry in modifiedPackages) { // Set LastUpdated after all IsLatest/IsLatestStable changes are complete to ensure // that we don't update rows where IsLatest/IsLatestStable hasn't changed. packageEntry.Entity.LastUpdated = DateTime.UtcNow; var isLatest = packageEntry.Entity.IsLatest ? 1 : 0; var isLatestStable = packageEntry.Entity.IsLatestStable ? 1 : 0; var key = packageEntry.Entity.Key; var originalIsLatest = Boolean.Parse(packageEntry.OriginalValues["IsLatest"].ToString()) ? 1 : 0; var originalIsLatestStable = Boolean.Parse(packageEntry.OriginalValues["IsLatestStable"].ToString()) ? 1 : 0; query.AppendLine($"UPDATE [dbo].[Packages]"); query.AppendLine($"SET [IsLatest] = {isLatest}, [IsLatestStable] = {isLatestStable}, [LastUpdated] = GETUTCDATE()"); query.AppendLine($"WHERE [Key] = {key}"); // optimistic concurrency check to prevent concurrent sets of latest/latestStable query.AppendLine($" AND [IsLatest] = {originalIsLatest} AND [IsLatestStable] = {originalIsLatestStable}"); // ensure new latest/latestStable was not concurrently unlisted/deleted if (packageEntry.Entity.IsLatest || packageEntry.Entity.IsLatestStable) { query.AppendLine($" AND [Listed] = 1 AND [Deleted] = 0"); } query.AppendLine($"SET @rowCount = @rowCount + @@ROWCOUNT"); } query.AppendLine("SELECT @rowCount"); using (var transaction = context.GetDatabase().BeginTransaction(IsolationLevel.ReadCommitted)) { var rowCount = await context.GetDatabase().ExecuteSqlCommandAsync(query.ToString()); if (rowCount == modifiedPackages.Count) { transaction.Commit(); return(true); } else { // RowCount will not match if one or more updates failed the concurrency check. This // likely means another thread is trying to clear the current IsLatest/IsLatestStable. transaction.Rollback(); return(false); } } }