private void processEnrollmentFile(string fileName, List<ParsedDataViewModel> result)
{
//Create new file log
var fileLog = new FileLog
{
FileDate = DateTime.UtcNow,
FileName = fileName
};
fileLogRepository.Add(fileLog);
//Create enrollment records
foreach (var enrollment in result)
{
var record = new Enrollment
{
UserId = enrollment.AgentId,
SocialSecurityNumber = enrollment.SocialSecurityNumber,
};
enrollmentRepository.AddEnrollment(record);
}
contextCoordinator.SaveChanges();
}
As you can see, first a file log is created and added to the context (via the fileLogRepository), enrollments are created and added to the context (via the enrollmentRepository), and everything is saved via an instance of a DataContextCoordinator class. That class is in the data access layer and wraps the context SaveChanges() method:
public class DataContextCoordinator : IDataContextCoordinatorUsing the example above, if there is an issue creating one of the enrollment records, no enrollment records nor a file log are created. But what happens if we need to include something in our processEnrollmentFile() method that executes outside of the context? Here's an example of how that same method could be altered:
{
public int SaveChanges()
{
try
{
return context.SaveChanges();
}
catch (Exception ex)
{
logger.ErrorException(ex);
throw;
}
}
}
private void processEnrollmentFile(string fileName, List<ParsedDataViewModel> result)In the updated method, we're now calling the ProcessEnrollments() method of the fileLogRepository. Let's say this method calls a stored procedure in our database that processes the enrollments. What happens if that stored procedure fails? We've already created and saved the file log and enrollment records. How can we include the execution of the stored procedure in a transaction that includes our context modifications?
{
//Create new file log
var fileLog = new FileLog
{
FileDate = DateTime.UtcNow,
FileName = fileName
};
fileLogRepository.Add(fileLog);
//Create enrollment records
foreach (var enrollment in result)
{
var record = new Enrollment
{
UserId = enrollment.AgentId,
SocialSecurityNumber = enrollment.SocialSecurityNumber,
};
enrollmentRepository.AddEnrollment(record);
}
contextCoordinator.SaveChanges();
//Process enrollments
fileLogRepository.ProcessEnrollments();
}
Let's first start by returning a result (1 if success, 0 if failed) from the stored procedure:
CREATE PROCEDURE [dbo].[usp_ProcessEnrollments]And here is our repository method that calls it:
@ReturnStatus int OUT
AS
--DO STUFF SET @ReturnStatus = 1; --Set to 0 if failed
END
public void ProcessEnrollments()Now we have to somehow wrap our entire enrollment processing block (the context changes and the stored procedure call) in a transaction. We start by adding the following method to the DataContextCoordinator class in our data access layer which accepts an Action to execute (if you need to return something, use a Func):
{
var response = new SqlParameter
{
ParameterName = "ReturnStatus",
DbType = DbType.Int32,
Direction = ParameterDirection.Output
};
var result = context.Database.SqlQuery<int>("EXEC usp_ProcessEnrollments @ReturnStatus", response);
var wasSuccessful = result.First() == 1;
if (!wasSuccessful)
{
var message = "An error occurred processing enrollments"; logger.Error(message);
throw new BadRequestException(message);
}
}
public void ExcecuteTransaction(Action action)Finally, we wrap our processEnrollmentFile() method in the business class in an action which we pass into the new ExecuteTransaction() method:
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
action();
transaction.Commit();
}
catch (Exception ex)
{
logger.ErrorException(ex);
transaction.Rollback();
throw;
}
}
}
public void UploadEnrollmentFile(string fileName, IList<ParsedDataViewModel> enrollmentData)Now if anything fails along the way, nothing will be persisted to the database. Additionally, this can be used in not just situations where calls are being made outside of your DbContext. If you need to call SaveChanges() on your DbContext numerous times for some reason, but want to undo everything if something goes wrong, this transaction solution can work for that as well.
{
var processEnrollmentFileAction = new Action<string, IList<ParsedDataViewModel>>(processEnrollmentFile);
contextCoordinator.ExcecuteTransaction(() => processEnrollmentFileAction(fileName, enrollmentData));
}
Using TransactionScope in the service layer, will accomplish the same thing and it's built in to the .Net framework. For example,
ReplyDeleteusing (TransactionScope tran = TransactionUtils.CreateTransactionScope())
{
_billingLogRepo.UpdateBillingLogAsPosted(billing_log_key);
tran.Complete();
}
That's what I recommend people use to handle transactions including date held in an EF DBContext.
I originally tried going down the TransactionScope path, but it's not recommended by the EF team anymore because it's so heavy. Plus TransactionScope requires DTC (Distributed Transaction Coordinator) which means enabling that feature on all servers participating in the transaction and regularly tweaking server settings. This solution is much more light weight.
Delete