/// <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)); }
/// <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)); }
/// <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)); }
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)); }
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)); }