Example #1
0
        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);
                }
            }
        }