/// <summary>
        /// Add user vote in store with retry attempts
        /// </summary>
        /// <param name="userVote">User vote instance with user and post id</param>
        /// <returns>True if operation executed successfully else false</returns>
        private async Task <bool> AddUserVoteAsync(UserVoteEntity userVote)
        {
            bool isUserVoteSavedSuccessful = false;

            try
            {
                // Update operation will throw exception if the column has already been updated
                // or if there is a transient error (handled by an Azure storage internally)
                isUserVoteSavedSuccessful = await this.userVoteStorageProvider.UpsertUserVoteAsync(userVote);
            }
            catch (StorageException ex)
            {
                if (ex.RequestInformation.HttpStatusCode == StatusCodes.Status412PreconditionFailed)
                {
                    this.logger.LogInformation("Optimistic concurrency violation – entity has changed since it was retrieved.");
                    throw;
                }
            }
#pragma warning disable CA1031 // catching generic exception to trace log error in telemetry and continue the execution
            catch (Exception ex)
#pragma warning restore CA1031 // catching generic exception to trace log error in telemetry and continue the execution
            {
                // log exception details to telemetry
                // but do not attempt to retry in order to avoid multiple vote count decrement
                this.logger.LogError(ex, "Exception occurred while reading post details.");
            }

            return(isUserVoteSavedSuccessful);
        }
        /// <summary>
        /// Stores or update user votes data.
        /// </summary>
        /// <param name="voteEntity">Holds user vote entity data.</param>
        /// <returns>A task that represents user vote entity data is saved or updated.</returns>
        private async Task <TableResult> StoreOrUpdateUserVoteAsync(UserVoteEntity voteEntity)
        {
            await this.EnsureInitializedAsync();

            TableOperation addOrUpdateOperation = TableOperation.InsertOrReplace(voteEntity);

            return(await this.GoodReadsCloudTable.ExecuteAsync(addOrUpdateOperation));
        }
Пример #3
0
        /// <summary>
        /// Stores or update user votes data in storage.
        /// </summary>
        /// <param name="voteEntity">Holds user vote entity data.</param>
        /// <returns>A task that represents user vote entity data is saved or updated.</returns>
        private async Task <TableResult> StoreOrUpdateEntityAsync(UserVoteEntity voteEntity)
        {
            await this.EnsureInitializedAsync();

            voteEntity = voteEntity ?? throw new ArgumentNullException(nameof(voteEntity));

            if (string.IsNullOrWhiteSpace(voteEntity.UserId) || string.IsNullOrWhiteSpace(voteEntity.IdeaId))
            {
                return(null);
            }

            TableOperation addOrUpdateOperation = TableOperation.InsertOrReplace(voteEntity);

            return(await this.CloudTable.ExecuteAsync(addOrUpdateOperation));
        }
Пример #4
0
        /// <summary>
        /// Store user vote details to storage.
        /// </summary>
        /// <param name="userVoteEntity">Represents user vote entity object.</param>
        /// <returns>A task that represents user vote entity data is added.</returns>
        public async Task <bool> AddUserVoteDetailsAsync(UserVoteEntity userVoteEntity)
        {
            try
            {
                userVoteEntity = userVoteEntity ?? throw new ArgumentNullException(nameof(userVoteEntity));

                if (userVoteEntity == null)
                {
                    return(false);
                }

                return(await this.userVoteStorageProvider.UpsertUserVoteAsync(userVoteEntity));
            }
            catch (Exception ex)
            {
                this.logger.LogError("Exception occurred while adding the user vote.", ex);
                throw;
            }
        }
        /// <summary>
        /// Stores or update user votes data.
        /// </summary>
        /// <param name="voteEntity">Holds user vote entity data.</param>
        /// <returns>A boolean that represents user vote entity is successfully saved/updated or not.</returns>
        public async Task <bool> UpsertUserVoteAsync(UserVoteEntity voteEntity)
        {
            var result = await this.StoreOrUpdateUserVoteAsync(voteEntity);

            return(result.HttpStatusCode == (int)HttpStatusCode.NoContent);
        }
        public async Task <IActionResult> DeleteVoteAsync(string postCreatedByUserId, string postId)
        {
            this.logger.LogInformation("call to delete user vote.");

            if (string.IsNullOrEmpty(postCreatedByUserId))
            {
                this.logger.LogError("Error while deleting vote. Parameter postCreatedByuserId is either null or empty.");
                return(this.BadRequest(new { message = "Parameter postCreatedByuserId is either null or empty." }));
            }

            if (string.IsNullOrEmpty(postId))
            {
                this.logger.LogError("Error while deleting vote. PostId is either null or empty.");
                return(this.BadRequest(new { message = "PostId is either null or empty." }));
            }

            bool isPostSavedSuccessful       = false;
            bool isUserVoteDeletedSuccessful = false;

            try
            {
                isUserVoteDeletedSuccessful = await this.userVoteStorageProvider.DeleteUserVoteAsync(postId, this.UserAadId);

                if (!isUserVoteDeletedSuccessful)
                {
                    this.logger.LogError($"Vote is not updated successfully for post {postId} by {postCreatedByUserId} ");
                    return(this.StatusCode(StatusCodes.Status500InternalServerError, "Vote is not updated successfully."));
                }

                // Retry if storage operation conflict occurs while updating post count.
                await this.retryPolicy.ExecuteAsync(async() =>
                {
                    isPostSavedSuccessful = await this.UpdateTotalCountAsync(postCreatedByUserId, postId, isUpvote: false);
                });
            }
#pragma warning disable CA1031 // catching generic exception to trace error in telemetry and return false value to client
            catch (Exception ex)
#pragma warning restore CA1031 // catching generic exception to trace error in telemetry and return false value to client
            {
                this.logger.LogError(ex, "Exception occured while deleting the user vote count.");
            }
            finally
            {
                // if user vote is not saved successfully
                // revert back the total post count
                if (isPostSavedSuccessful)
                {
                    // run Azure search service to refresh the index for getting latest vote count
                    await this.postSearchService.RunIndexerOnDemandAsync();
                }
                else
                {
                    UserVoteEntity userVote = new UserVoteEntity
                    {
                        UserId = this.UserAadId,
                        PostId = postId,
                    };

                    // add the user vote back to table
                    await this.retryPolicy.ExecuteAsync(async() =>
                    {
                        await this.AddUserVoteAsync(userVote);
                    });
                }
            }

            return(this.Ok(isPostSavedSuccessful));
        }
        public async Task <IActionResult> AddVoteAsync(string postCreatedByUserId, string postId)
        {
            this.logger.LogInformation("call to add user vote.");

            if (string.IsNullOrEmpty(postCreatedByUserId))
            {
                this.logger.LogError("Error while deleting vote. Parameter postCreatedByuserId is either null or empty.");
                return(this.BadRequest(new { message = "Parameter postCreatedByuserId is either null or empty." }));
            }

            if (string.IsNullOrEmpty(postId))
            {
                this.logger.LogError("Error while deleting vote. PostId is either null or empty.");
                return(this.BadRequest(new { message = "PostId is either null or empty." }));
            }

            bool isUserVoteSavedSuccessful = false;
            bool isPostSavedSuccessful     = false;

            try
            {
#pragma warning disable CA1062 // post details are validated by model validations for null check and is responded with bad request status
                var userVoteForPost = await this.userVoteStorageProvider.GetUserVoteForPostAsync(this.UserAadId, postId);

#pragma warning restore CA1062 // post details are validated by model validations for null check and is responded with bad request status

                if (userVoteForPost == null)
                {
                    UserVoteEntity userVote = new UserVoteEntity
                    {
                        UserId = this.UserAadId,
                        PostId = postId,
                    };

                    await this.retryPolicy.ExecuteAsync(async() =>
                    {
                        isUserVoteSavedSuccessful = await this.AddUserVoteAsync(userVote);
                    });

                    if (!isUserVoteSavedSuccessful)
                    {
                        this.logger.LogError($"User vote is not updated successfully for post {postId} by {this.UserAadId} ");
                        return(this.StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while saving user vote."));
                    }

                    // Retry if storage operation conflict occurs during updating user vote count.
                    await this.retryPolicy.ExecuteAsync(async() =>
                    {
                        isPostSavedSuccessful = await this.UpdateTotalCountAsync(postCreatedByUserId, postId, isUpvote: true);
                    });
                }
            }
#pragma warning disable CA1031 // catching generic exception to trace error in telemetry and return false value to client
            catch (Exception ex)
#pragma warning restore CA1031 // catching generic exception to trace error in telemetry and return false value to client
            {
                this.logger.LogError(ex, "Exception occurred while updating user vote.");
            }
            finally
            {
                if (isPostSavedSuccessful)
                {
                    // run Azure search service to refresh the index for getting latest vote count
                    await this.postSearchService.RunIndexerOnDemandAsync();
                }
                else
                {
                    // revert user vote entry if the post total count didn't saved successfully
                    this.logger.LogError($"Post vote count is not updated successfully for post {postId} by {this.UserAadId} ");

                    // exception handling is implemented in method and no additional check is required
                    var isUserVoteDeletedSuccessful = await this.userVoteStorageProvider.DeleteUserVoteAsync(postId, this.UserAadId);

                    if (isUserVoteDeletedSuccessful)
                    {
                        this.logger.LogInformation("Vote revoked from user table");
                    }
                    else
                    {
                        this.logger.LogError("Vote cannot be revoked from user table");
                    }
                }
            }

            return(this.Ok(isPostSavedSuccessful));
        }
Пример #8
0
        public async Task <IActionResult> DeleteVoteAsync(string postCreatedByUserId, string postId)
        {
            this.logger.LogInformation("call to delete user vote.");

            if (string.IsNullOrEmpty(postCreatedByUserId))
            {
                this.logger.LogError($"Error while deleting vote. Parameter {nameof(postCreatedByUserId)} is either null or empty.");
                return(this.BadRequest(new { message = $"Parameter {nameof(postCreatedByUserId)} is either null or empty." }));
            }

            if (string.IsNullOrEmpty(postId))
            {
                this.logger.LogError($"Error while deleting vote. {nameof(postId)} is either null or empty.");
                return(this.BadRequest(new { message = $"{nameof(postId)} is either null or empty." }));
            }

            bool isPostSavedSuccessful       = false;
            bool isUserVoteDeletedSuccessful = false;

            // Note: the implementation here uses Azure table storage for handling votes
            // in posts and user vote tables.Table storage are not transactional and there
            // can be instances where the vote count might be off.The table operations are
            // wrapped with retry policies in case of conflict or failures to minimize the risks.
            try
            {
                isUserVoteDeletedSuccessful = await this.userVoteStorageProvider.DeleteEntityAsync(postId, this.UserAadId);

                if (!isUserVoteDeletedSuccessful)
                {
                    this.logger.LogError($"Vote is not updated successfully for post {postId} by {postCreatedByUserId} ");
                    return(this.StatusCode(StatusCodes.Status500InternalServerError, "Vote is not updated successfully."));
                }

                // Retry if storage operation conflict occurs while updating post count.
                await this.retryPolicy.ExecuteAsync(async() =>
                {
                    isPostSavedSuccessful = await this.UpdateTotalCountAsync(postCreatedByUserId, postId, isUpvote: false);
                });
            }
#pragma warning disable CA1031 // catching generic exception to trace error in telemetry and return false value to client
            catch (Exception ex)
#pragma warning restore CA1031 // catching generic exception to trace error in telemetry and return false value to client
            {
                this.logger.LogError(ex, "Exception occurred while deleting the user vote count.");
            }
            finally
            {
                // if user vote is not saved successfully
                // revert back the total post count
                if (isPostSavedSuccessful)
                {
                    // run Azure search service to refresh the index for getting latest vote count
                    await this.teamIdeaSearchService.RunIndexerOnDemandAsync();
                }
                else
                {
                    UserVoteEntity userVote = new UserVoteEntity
                    {
                        UserId = this.UserAadId,
                        IdeaId = postId,
                    };

                    // add the user vote back to storage
                    await this.retryPolicy.ExecuteAsync(async() =>
                    {
                        await this.AddUserVoteAsync(userVote);
                    });
                }
            }

            return(this.Ok(isPostSavedSuccessful));
        }
Пример #9
0
        public async Task <IActionResult> AddVoteAsync(string postCreatedByUserId, string postId)
        {
            this.logger.LogInformation("call to add user vote.");

#pragma warning disable CA1062 // post details are validated by model validations for null check and is responded with bad request status
            var userVoteForPost = await this.userVoteStorageProvider.GetUserVoteForPostAsync(this.UserAadId, postId);

#pragma warning restore CA1062 // post details are validated by model validations for null check and is responded with bad request status

            if (userVoteForPost == null)
            {
                UserVoteEntity userVote = new UserVoteEntity
                {
                    UserId = this.UserAadId,
                    PostId = postId,
                };

                PostEntity postEntity            = null;
                bool       isPostSavedSuccessful = false;

                // Retry if storage operation conflict occurs during updating user vote count.
                await this.retryPolicy.ExecuteAsync(async() =>
                {
                    try
                    {
                        postEntity = await this.postStorageProvider.GetPostAsync(postCreatedByUserId, userVote.PostId);

                        // increment the vote count
                        // if the execution is retried, then get the latest vote count and increase it by 1
                        postEntity.TotalVotes += 1;

                        isPostSavedSuccessful = await this.postStorageProvider.UpsertPostAsync(postEntity);
                    }
                    catch (StorageException ex)
                    {
                        if (ex.RequestInformation.HttpStatusCode == StatusCodes.Status412PreconditionFailed)
                        {
                            this.logger.LogError("Optimistic concurrency violation – entity has changed since it was retrieved.");
                            throw;
                        }
                    }
#pragma warning disable CA1031 // catching generic exception to trace log error in telemetry and continue the execution
                    catch (Exception ex)
#pragma warning restore CA1031 // catching generic exception to trace log error in telemetry and continue the execution
                    {
                        // log exception details to telemetry
                        // but do not attempt to retry in order to avoid multiple vote count increment
                        this.logger.LogError(ex, "Exception occurred while reading post details.");
                    }
                });

                if (!isPostSavedSuccessful)
                {
                    this.logger.LogError($"Vote is not updated successfully for post {postId} by {this.UserAadId} ");
                    return(this.StatusCode(StatusCodes.Status500InternalServerError, "Vote is not updated successfully."));
                }

                bool isUserVoteSavedSuccessful = false;

                this.logger.LogInformation($"Post vote count updated for PostId:{postId}");
                isUserVoteSavedSuccessful = await this.userVoteStorageProvider.UpsertUserVoteAsync(userVote);

                // if user vote is not saved successfully
                // revert back the total post count
                if (!isUserVoteSavedSuccessful)
                {
                    await this.retryPolicy.ExecuteAsync(async() =>
                    {
                        try
                        {
                            postEntity             = await this.postStorageProvider.GetPostAsync(postCreatedByUserId, userVote.PostId);
                            postEntity.TotalVotes -= 1;

                            // Update operation will throw exception if the column has already been updated
                            // or if there is a transient error (handled by an Azure storage)
                            await this.postStorageProvider.UpsertPostAsync(postEntity);
                            await this.postSearchService.RunIndexerOnDemandAsync();
                        }
                        catch (StorageException ex)
                        {
                            if (ex.RequestInformation.HttpStatusCode == StatusCodes.Status412PreconditionFailed)
                            {
                                this.logger.LogError("Optimistic concurrency violation – entity has changed since it was retrieved.");
                                throw;
                            }
                        }
#pragma warning disable CA1031 // catching generic exception to trace log error in telemetry and continue the execution
                        catch (Exception ex)
#pragma warning restore CA1031 // catching generic exception to trace log error in telemetry and continue the execution
                        {
                            // log exception details to telemetry
                            // but do not attempt to retry in order to avoid multiple vote count decrement
                            this.logger.LogError(ex, "Exception occurred while reading post details.");
                        }
                    });
                }
                else
                {
                    this.logger.LogInformation($"User vote added for user{this.UserAadId} for PostId:{postId}");
                    await this.postSearchService.RunIndexerOnDemandAsync();

                    return(this.Ok(true));
                }
            }

            return(this.Ok(false));
        }