public async Task <Dictionary <string, object> > GetSyncResponse(int eventId, int teamId, int puzzleId, List <int> query_puzzle_ids, int?min_solve_count, string annotations, string last_sync_time) { SyncResponse response = new SyncResponse(); // Get information for the puzzle that's being sync'ed. Puzzle thisPuzzle = await context.Puzzles.Where(puz => puz.ID == puzzleId).FirstOrDefaultAsync(); if (thisPuzzle == null) { response.AddError("Could not find the puzzle you tried to sync."); return(response.GetResult()); } // Check to make sure that they're not trying to sync a puzzle and event that don't go together. // That could be a security issue, allowing them to unlock pieces for a puzzle using the progress // they made in a whole different event! if (thisPuzzle.Event.ID != eventId) { response.AddError("That puzzle doesn't belong to that event."); return(response.GetResult()); } // Get the site version for this puzzle, so that if it's changed since the last time the // requester sync'ed, the requester will know to reload itself. response.SetSiteVersion(GetSiteVersion(thisPuzzle)); // Decode the request. If there are any errors, return an error response. DecodedSyncRequest request = new DecodedSyncRequest(query_puzzle_ids, min_solve_count, annotations, last_sync_time, thisPuzzle.MaxAnnotationKey, ref response); // Do any processing that requires fetching the list of all puzzles this team has // solved. Pass thisPuzzle.Group as the groupExcludedFromSolveCount parameter so that, // when counting solves, we don't count solves in the same group as this puzzle as part // of the solve count. await HandleSyncAspectsRequiringListOfSolvedPuzzles(request, response, thisPuzzle.Group, teamId, eventId); // Store any annotations the requester provided await StoreAnnotations(request, response, thisPuzzle.ID, teamId); // Fetch and return any annotations that the requester may not have yet await GetAnnotationsRequesterLacks(request, response, thisPuzzle.ID, teamId); return(response.GetResult()); }
/// <summary> /// This routine updates a single existing annotation. As we do so, we increment /// its version number and update its timestamp. /// /// This routine is only meant to be called when we know an annotation exists /// with the given puzzle ID, team ID, and key. In other words, this routine /// is called when we need to update that annotation. /// </summary> private async Task UpdateOneAnnotation(SyncResponse response, int puzzleId, int teamId, int key, string contents) { // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal" // Entity Framework database functions. The answer is that we need to atomically // update the Version field of the record, and Entity Framework has no way of // expressing that directly. // // The reason we want to update the version number atomically is that we rely // on the version number being a unique identifier of an annotation. We don't // want the following scenario: // // Alice tries to set the annotation for key 17 to A, and simultaneously Bob // tries to set it to B. Each reads the current version number, finds it to be // 3, and updates the annotation to have version 4. Both of these updates may // succeed, but one will overwrite the other; let's say Bob's write happens last // and "wins". So Alice may believe that version 4 is A when actually version 4 // is B. When Alice asks for the current version, she'll be told it's version 4, // and Alice will believe this means it's A. So Alice will believe that A is // what's stored in the database even though it's not. Alice and Bob's computers // will display different annotations for the same key, indefinitely. // // Note that we need a version number because the timestamp isn't guaranteed to // be unique. So in the example above Alice and Bob might wind up updating with // the same timestamp. // // You may also wonder why we use DateTime.Now instead of letting the database // assign the timestamp itself. The reason is that the database might be running // on a different machine than the puzzle server, and it might be using a different // time zone. try { var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key"; int result = await context.Database.ExecuteSqlRawAsync(sqlCommand, new SqlParameter("@Contents", contents), new SqlParameter("@Timestamp", DateTime.Now), new SqlParameter("@PuzzleID", puzzleId), new SqlParameter("@TeamID", teamId), new SqlParameter("@Key", key)); if (result != 1) { response.AddError("Annotation update failed."); } } catch (DbUpdateException) { response.AddError("Encountered error while trying to update annotation."); } catch (Exception) { response.AddError("Miscellaneous error while trying to update annotation."); } }
public DecodedSyncRequest(List <int> query_puzzle_ids, int?min_solve_count, string annotations, string last_sync_time, int maxAnnotationKey, bool query_all_in_group, ref SyncResponse response) { // The query_puzzle_ids, min_solve_count, and query_all_in_group fields are already perfectly fine, so just copy them. QueryPuzzleIds = query_puzzle_ids; MinSolveCount = min_solve_count; QueryAllInGroup = query_all_in_group; // The last_sync_time field is a JSON-converted DateTime. Or at least, it's supposed to be; // we need to check. Note that this string is one that we (the server) encoded. We do DateTime // encoding and decoding on the server so as not to worry about different browsers encoding // DateTimes differently. if (last_sync_time == null) { LastSyncTime = null; } else { try { LastSyncTime = JsonConvert.DeserializeObject <DateTime>(last_sync_time); } catch (JsonException) { response.AddError("Could not deserialize last sync time."); LastSyncTime = null; } } // The annotations field is a JSON-converted list of objects, each with an integer key and // string contents. Or at least, it's supposed to be; we have to check it carefully. AnnotationRequests = new List <AnnotationRequest>(); if (annotations != null) { List <AnnotationRequest> uncheckedAnnotationRequests; try { // First, decode it into a list using the JSON deserializer. uncheckedAnnotationRequests = JsonConvert.DeserializeObject <List <AnnotationRequest> >(annotations); } catch (JsonException) { response.AddError("Could not deserialize annotations list."); uncheckedAnnotationRequests = new List <AnnotationRequest>(); } // Check each of the elements for validity. foreach (var annotation in uncheckedAnnotationRequests) { if (annotation.key < 1) { response.AddError("Found non-positive key in annotation."); continue; } if (annotation.key > maxAnnotationKey) { response.AddError("Found too-high key in annotation."); continue; } if (annotation.contents.Length > 255) { response.AddError("Found contents in annotation longer than 255 bytes."); continue; } AnnotationRequests.Add(annotation); } } }
/// <summary> /// This routine stores in the database any annotations the requester has uploaded. /// </summary> private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) { if (request.AnnotationRequests == null) { return; } foreach (var annotationRequest in request.AnnotationRequests) { // Try to generate this as a new annotation, with version 1. Annotation annotation = new Annotation(); annotation.PuzzleID = puzzleId; annotation.TeamID = teamId; annotation.Key = annotationRequest.key; annotation.Version = 1; annotation.Contents = annotationRequest.contents; annotation.Timestamp = DateTime.Now; try { context.Annotations.Add(annotation); await context.SaveChangesAsync(); } catch (DbUpdateException) { // If the insert fails, there must already be an annotation there with the // same puzzle ID, team ID, and key. So we need to update the existing one. // As we do so, we increment its version number and update its timestamp. // // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal" // Entity Framework database functions. The answer is that we need to atomically // update the Version field of the record, and Entity Framework has no way of // expressing that directly. // // The reason we want to update the version number atomically is that we rely // on the version number being a unique identifier of an annotation. We don't // want the following scenario: // // Alice tries to set the annotation for key 17 to A, and simultaneously Bob // tries to set it to B. Each reads the current version number, finds it to be // 3, and updates the annotation to have version 4. Both of these updates may // succeed, but one will overwrite the other; let's say Bob's write happens last // and "wins". So Alice may believe that version 4 is A when actually version 4 // is B. When Alice asks for the current version, she'll be told it's version 4, // and Alice will believe this means it's A. So Alice will believe that A is // what's stored in the database even though it's not. Alice and Bob's computers // will display different annotations for the same key, indefinitely. // // Note that we need a version number because the timestamp isn't guaranteed to // be unique. So in the example above Alice and Bob might wind up updating with // the same timestamp. // // You may also wonder why we use DateTime.Now instead of letting the database // assign the timestamp itself. The reason is that the database might be running // on a different machine than the puzzle server, and it might be using a different // time zone. // First, detach the annotation from the context so the context doesn't think the annotation is in the database. context.Entry(annotation).State = EntityState.Detached; try { var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key"; int result = await context.Database.ExecuteSqlCommandAsync(sqlCommand, new SqlParameter("@Contents", annotationRequest.contents), new SqlParameter("@Timestamp", DateTime.Now), new SqlParameter("@PuzzleID", puzzleId), new SqlParameter("@TeamID", teamId), new SqlParameter("@Key", annotationRequest.key)); if (result != 1) { response.AddError("Annotation update failed."); } } catch (DbUpdateException) { response.AddError("Encountered error while trying to update annotation."); } catch (Exception) { response.AddError("Miscellaneous error while trying to update annotation."); } } } }