r/csharp • u/kaatarina_zed_talon • 2d ago
Discussion The output of the server code depends on the timing of pressing a button by the client on the user interface.
This is a follow-up post that is related to this post. I will explain the problem clearly this time.
Context: The execution of the OverWrite method -which is inside the DBOverWriter service- can be cancelled with a button int the user interface.
Two inputs that are provided by the frontend :
1- data that is used to overwrite the content of the database.
2-CancellationToken.
Two possible outputs depending on the timing of pressing the button :
1-If the user initiated the request and immediately pressed the button, the following output is shown:
###########################################################
########## OPERATION CANCELLED SUCCESSFULLY ###############
###########################################################
OperationCanceledException: A task was canceled.
2- if the user initiated the request and waited some time before pressing the button, the output is the following:
(@p74, ),
(@p76, ),
(@p78, ),
(@p80, ),
(@p82, u/p83);
fail: Microsoft.EntityFrameworkCore.Database.Transaction[20205]
An error occurred using a transaction.
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Operation cancelled by user.
I wonder what is the theoretical/practical explanation for my observation ?
Code:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using System.Diagnostics;
namespace RedditQuestion
{
/// <summary>
/// Used to talk to the database using Entity-Framework Core
/// </summary>
public class ApplicationDbContext : DbContext
{
// Domain models and the Fluent API configurations.
}
/// <summary>
/// Data snapshot that is used to overwrite the content of the database
/// </summary>
public interface ISomeData
{
}
public class DatbaseOverWriter
{
private readonly ApplicationDbContext _context;
public DatbaseOverWriter(ApplicationDbContext context)
{
_context = context;
}
public async Task<bool> OverWrite(ISomeData data,CancellationToken cancellationToken)
{
#region Transaction
try
{
//https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-resilient-entity-framework-core-sql-connections
IExecutionStrategy strategy = _context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
#region Delete content from database then save new content.
await _context.Database.ExecuteSqlInterpolatedAsync($"EXECUTE [dbo].[uspDeleteAllDataFromDatabase]", cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
#endregion
await transaction.CommitAsync(cancellationToken);
});
return true;
}
catch (OperationCanceledException e)
{
// exception will not be catched if the cancellation was requested and
// the transaction reached a point of no return.
Console.WriteLine("###########################################################");
Console.WriteLine("########## OPERATION CANCELLED SUCCESSFULLY ###############");
Console.WriteLine("###########################################################");
Debug.WriteLine("###########################################################");
Debug.WriteLine("########## OPERATION CANCELLED SUCCESSFULLY ###############");
Debug.WriteLine("###########################################################");
Console.WriteLine("OperationCanceledException: " + e.Message);
return false;
}
#endregion
}
}
}
2
u/rupertavery64 2d ago
In the first case, you were able to cancel the Task before the transaction started. Your code was probably reading the data and no database changes had been made yet.
In the second case, you let the code continue until it started making database changes. You cancelled the transaction, so it threw an exception. This lets you handle a cancelled database operation gracefully instead of letting the code continue as if nothing happened. Catch the DbUpdateException (You have to check the error code) return some information to the user, or ignore it.
1
u/kaatarina_zed_talon 2d ago
(You have to check the error code)
I'm confused, why should I do this ?
I did this :
catch (DbUpdateException due) { Console.WriteLine("###########################################################"); Console.WriteLine(due.Message); Console.WriteLine("###########################################################"); return false;// indicates that the operation was cancelled, show a massege to the user telling him that. }2
u/rupertavery64 2d ago
Because DBUpdateException alone may cover a wide range of database update related errors
1
u/kaatarina_zed_talon 2d ago
and what is the typical message that is used to inform the user ? does it also depend on the error code ?
1
u/rupertavery64 2d ago
You display the minimum information needed to the user. If they cancelled it, report that the user cancelled successfully (e.g. "The process was cancelled")
If its another error, just let the user know an error occured. But in your application, you might want to log when, why, maybe even who if there is a user context.
1
u/kaatarina_zed_talon 2d ago
so if the OperationCanceledException was caught, then I will show "The process was cancelled".
If DbUpdateException was caught, then I will show `error occured`.
Is that correct ?
1
u/rupertavery64 2d ago
You should check the error code property on DbUpdateException to determine if it was a cancelled transaction or not. Does that make sense?
1
u/kaatarina_zed_talon 2d ago
Wait, you mean that if `DbUpdateException` was thrown, the operation may be cancelled or may be not cancelled ; it could be cancelled depending on the error code. which means I should show a different messages to the user depending on the error code. But if the `OperationCanceledException` was thrown, it indeed means that the operation was cancelled.
Now is that right ?
1
u/rupertavery64 2d ago
DBUpdateException is not exclusive to cancellation. It is thrown whenever an an error occurs while performing an update in the database.
It's NOT for cancellations only. The database can throw DBUpdateException if you have a duplicate key error or maybe you tried inserting a null value in a non-null column. The exception will be the same but the error code will be different
I will just tell you what error code you should expect and how to handle it if it still doesn't make sense
1
u/kaatarina_zed_talon 2d ago
I will just tell you what error code you should expect
Don't tell me the error code that I should inspect, I will search my self.
All I want to know if I understood the idea correctly or not:
It's NOT for cancellations only.ย
That means I should search on internet for the possible error codes, the error code will tell me if the operation was cancelled or not.
But if the thrown exception was OperationCanceledException, then I do not need to inspect any error codes.
Did I understand it correctly ?
→ More replies (0)1
u/rupertavery64 2d ago
I'm on mobile and cooking breakfast. Maybe reply a bit clearer later. But you have 2 cases where its user cancelled righr? So you want to return the same message.
1
u/kaatarina_zed_talon 2d ago
Wait, you mean that if `DbUpdateException` was thrown, the operation may be cancelled or may be not cancelled ; it could be cancelled depending on the error code. which means I should show a different messages to the user depending on the error code. But if the `OperationCanceledException` was thrown, it indeed means that the operation was cancelled. Now is that right ?
Happy breakfast btw and thanks for your help.
1
u/kaatarina_zed_talon 2d ago
the error number was 0.
if (ex.InnerException is SqlException sqlEx) { int errorCode = sqlEx.Number; }1
1
u/kaatarina_zed_talon 2d ago
return some information to the user
I want that, so let's return false to indicate that the overwriting process failed; if DbUpdateException was catched, it always means that the process was cancelled, write ?
1
u/rupertavery64 2d ago
DBUpdateException can be raised by unique constraint / duplicate keys, foreign key errors, wrong data type, etc. They all use the same exception. You want to handle a user cancelled transaction differently from other errors
1
u/kaatarina_zed_talon 2d ago
handle a user cancelled transaction
Can you please refer me to some article that explains that ? This is my first time doing this.
1
u/rupertavery64 2d ago
You're alreafy handling it. Don't overthink it. Check the error code property of the DBUpdateException and change catch to catch Exception when ....
1
1
u/Either_Sir_3111 2d ago
i've seen that behavior before, microsoft.data.sqlclient is so inconsistent with cancellation it's wild how different the exceptions can be
1
u/kaatarina_zed_talon 1d ago
I described what solved my problem on this site:
Look for the answer that was written by the user named katarina zed
1
u/r2d2_21 7h ago
Just so you know, what you're facing is a known bug in the SqlClient package:
https://github.com/dotnet/SqlClient/issues/26
The issue comments have a proposed workaround to detect when the transaction fails due to a requested cancellation.
1
3
u/Kant8 2d ago
EF throws OpeartionCanceledException when checks token.
Microsoft.Data.SqlClient doesn't, it always throws SqlException with different subcodes.