From time to time I get a Azure database transaction timeout from a .NET core application using entity framework. What's the hierachy of setting the command timeout? There are three options: a) within the connection string, b) Options, Query execution settings, c) buidler, AddDbContext options. Since the timeout never really precisely happens at the configured timeout settings I would like to know.
Apparently just setting it with the connection string doesn't work.
This is the transaction code:
protected async Task UpdateDatabaseAsync() {
Logger.LogInformation( "Updating database..." );
// convert BlockingCollection to list
var scrapeResults = ScrapeResultsModel.ToList();
// check if list is empty
if( scrapeResults.Count == 0 )
Logger.LogInformation( "List is empty, nothing to update!" );
else {
// create a TransactionScope to ensure all or nothing
// Not using "using" here for DB context/connection is recommended by MS because of DI container.
using var transactionScope = new TransactionScope( TransactionScopeAsyncFlowOption.Enabled );
try {
// load all objects by ObjectId from _scrapeResultModel from DB
List<string> objectIds = new();
foreach( var result in scrapeResults ) {
objectIds.Add( result.ObjectId );
}
var query = DbContext.RealEstates.Where( x => objectIds.Contains( x.ObjectId ) );
var existingRecords = await query.ToListAsync();
// compare lists
for( int i = 0; i < existingRecords.Count; i++ ) {
for( int j = scrapeResults.Count - 1; j >= 0; j-- ) {
// exclusions for scraped objects here
// remove cemetery objects
var title = scrapeResults[ j ].Title?.ToLower();
var fullText = scrapeResults[ j ].FullTextEs?.ToLower();
if( fullText != null && title != null &&
( title.Contains( "cementerio" ) || fullText.Contains( "cementerio" ) ) ) {
Logger.LogInformation(
$"Object {scrapeResults[ j ].ObjectId} is a cemetery/cementerio, removed from list! Title: {scrapeResults[ j ].Title}" );
scrapeResults.RemoveAt( j );
continue;
}
// if objects have the same ObjectId compare prices
if( existingRecords[ i ].ObjectId == scrapeResults[ j ].ObjectId ) {
if( existingRecords[ i ].PriceTotal == scrapeResults[ j ].PriceTotal &&
existingRecords[ i ].RentMonthly == scrapeResults[ j ].RentMonthly ) {
// if object already exists and price has not changed, delete from list
Logger.LogInformation(
$"Object {scrapeResults[ j ].ObjectId} already exists, removed from list." );
scrapeResults.RemoveAt( j );
}
else {
// Mark the new price as changed.
// Leave the object in list and it will be added to DB.
// Multiple entries will be created for the same object!
// for sale:
if( scrapeResults[ j ].Type == 1 ) {
scrapeResults[ j ].LastChangedField += "PriceTotal,";
scrapeResults[ j ].DateLastChange = DateTime.UtcNow;
decimal.TryParse( existingRecords[ i ].PriceTotal, out var oldPrice );
decimal.TryParse( scrapeResults[ j ].PriceTotal, out var newPrice );
if( oldPrice > 0 ) {
var discount = decimal.Round( ( newPrice - oldPrice ) / oldPrice * 100, 2 );
scrapeResults[ j ].Discount = discount.ToString( CultureInfo.CurrentCulture );
Logger.LogInformation(
$"New price for object {scrapeResults[ j ].ObjectId}: {scrapeResults[ j ].PriceTotal}" );
}
}
// for rent:
else if( scrapeResults[ j ].Type == 2 ) {
scrapeResults[ j ].LastChangedField += "RentMonthly,";
scrapeResults[ j ].DateLastChange = DateTime.UtcNow;
decimal.TryParse( existingRecords[ i ].RentMonthly, out var oldPrice );
decimal.TryParse( scrapeResults[ j ].RentMonthly, out var newPrice );
if( oldPrice > 0 ) {
var discount = decimal.Round( ( oldPrice - newPrice ) / oldPrice * 100, 2 );
scrapeResults[ j ].Discount = discount.ToString( CultureInfo.CurrentCulture );
Logger.LogInformation(
$"New price for object {scrapeResults[ j ].ObjectId}: {scrapeResults[ j ].RentMonthly}" );
}
}
else
throw new Exception( "Unknown type!" );
}
}
}
}
// convert image BlockingCollection to list
var scrapeResultsImage = ScrapeResultsImage.ToList();
// add related images to real estate objects
for( int i = 0; i < scrapeResults.Count; i++ ) {
Logger.LogInformation( $"New image to DB for RealEstate objectId: {scrapeResults[ i ].ObjectId}" );
for( int j = 0; j < scrapeResultsImage.Count; j++ ) {
if( scrapeResults[ i ].ObjectId == scrapeResultsImage[ j ].ObjectId ) {
Logger.LogInformation(
$"New image to DB, Guid = {scrapeResultsImage[ j ].Guid}, ObjectId = {scrapeResults[ i ].ObjectId}" );
scrapeResults[ i ].Images.Add( scrapeResultsImage[ j ] );
}
}
}
// add completed list to DB
Logger.LogInformation( $"Adding complete list with {scrapeResults.Count} items to DB..." );
if( scrapeResults.Count > 0 ) {
await DbContext.RealEstates.AddRangeAsync( scrapeResults );
// Microsoft: SaveChangesAsync() is not recommended for use with TransactionScope.
var result = DbContext.SaveChanges();
transactionScope.Complete();
Logger.LogInformation( $"List added to DB, commit! Results = {result}" );
}
}
catch( Exception e ) {
Logger.LogError( e, "Error updating database, rollback!" );
}
}
}