Scenario
I'm writing a system to carry out exams. The person in charge of the exam (Invigilator) starts the exam, and at that point, the people taking the exam (Candidates) are permitted to start. If a candidate tries to start prematurely they receive a message telling them to wait for the invigilator. Both start times are logged (Exam Start and Candidate Start).
When the invigilator starts the exam we set the exam start time
for the exam, we then find out how much time each candidate is allowed to have on the exam, and add it to the exam start time
, thus giving us what I call their scheduled finish
.
public void SetExamStart(Exam exam, List<ExamCandidate> examCandidates)
{
DateTime startTime = _timeService.GetCurrentDateTime();
exam.ExamStarted = startTime;
_work.ExamRepository.Update(exam);
examCandidates.ForEach(ec =>
{
ExamPaper examPaper = ec.ExamPaper;
if (!examPaper.ExamDuration.HasValue)
{
return;
}
Int32 examPaperDuration = examPaper.ExamDuration.Value;
DateTime scheduledFinish = startTime.AddMinutes(examPaperDuration);
ec.ScheduledFinish = scheduledFinish;
_work.ExamCandidateRepository.Update(ec);
});
}
The calls to the repositories are within the same transaction. ConnectionService
is dependency injected into the repository, and the repository is injected into UnitOfWork
(_work), both on per web request basis.
public virtual void Update(TEntity entity)
{
IDbConnection connection = ConnectionService.Connection;
try
{
connection.Execute(_queryGenerator.UpdateQuery, entity, ConnectionService.Transaction);
}
catch (SqlException ex)
{
throw new DataAccessException("An error occured on query execution", ex);
}
}
When a candidate attempts to start:
public void StartCandidateExam(Guid examCandidateId)
{
ExamCandidate examCandidate = _work.ExamCandidateRepository.Get(examCandidateId);
Exam exam = examCandidate.Exam;
if (!exam.ExamStarted.HasValue) {
throw new ExamNotStartedException("Please wait for the invigilator to start the exam");
}
_candidateExamService.StartExam(examCandidate);
_work.SaveChanges();
}
public void StartExam(ExamCandidate examCandidate)
{
DateTime currentTime = _timeService.GetCurrentDateTime();
examCandidate.Started = currentTime;
_work.ExamCandidateRepository.Update(examCandidate);
}
Throughout this project I use a Lazy<T>
on some entities. Such as the ExamPaper
property on the ExamCandidate
entity. This is shown below:
public class ExamCandidate
{
//other properties
public DateTime? ScheduledFinish { get; set; }
public int ExamPaperID { get; set; }
public LazyEntity<ExamPaper> ExamPaper { get; set; }
}
public class LazyEntity<T> : Lazy<T>
{
public LazyEntity(Func<T> func) : base(func) {}
public static implicit operator T(LazyEntity<T> lazy)
{
return lazy.Value;
}
}
// setting the lazy in the exam candidate repository
examCandidate.ExamPaper = new LazyEntity<ExamPaper>(() =>
{
return Work.ExamPaperRepository.Get(examCandidate.ExamPaperID);
});
Problem:
A candidate started their exam very soon after the invigilator which has resulted in their ScheduledFinish not being set. This has worked fine for the other candidates that started a few seconds after.
Here are the logs:
+------------------------+-------------------------+
| Log Type | Log Date |
+------------------------+-------------------------+
| Exam Started | 2018-03-08 15:00:58.370 |
| Candidate Exam Started | 2018-03-08 15:00:58.387 |
+------------------------+-------------------------+
+-------------------------+-------------------------+--------+-------------+
| Started | ScheduledFinish | ExamID | ExamPaperID |
+-------------------------+-------------------------+--------+-------------+
| 2018-03-08 15:00:58.387 | NULL | 42 | 34 |
| 2018-03-08 15:01:01.727 | 2018-03-08 15:30:58.370 | 42 | 34 |
| 2018-03-08 15:01:02.507 | 2018-03-08 15:30:58.370 | 42 | 56 |
| 2018-03-08 15:01:02.770 | 2018-03-08 15:30:58.370 | 42 | 56 |
| 2018-03-08 15:01:02.960 | 2018-03-08 15:30:58.370 | 42 | 34 |
+-------------------------+-------------------------+--------+-------------+
I cannot see a reason for this, the starting of the exam and the setting of each scheduled finish is done in the same operation, under the same transaction and a candidate cannot start until this has happened.
Is there something I'm missing?
--Edit To clarify, this problem has happened twice in a little under a year. Both times the candidate has started very soon after the invigilator started.