Best way to handle transaction errors? - asp.net-core

What's the best way to handle transaction errors in Asp.net core and entity framework?
At this time I have come with something like:
using (var transaction = _dbContext.Database.BeginTransaction())
{
try
{
await _dbContext.MyTable1.AddAsync(table1Entity);
await _dbContext.SaveChangesAsync();
Entity2 e2 = new Entity2();
e2.Table1Id = table1Entity.Id;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return new CreatedAtRouteResult(...);
}
catch (Exception ex)
{
System.Console.Write(ex);
await transaction.RollbackAsync();
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
return Problem(
detail: context.Error.StackTrace,
title: context.Error.Message);
}
}
But don't really know if this is a good practice. How would you do this?

There is nothing wrong with the way you are handling the transactions, but there are some improvements you can make here:
Remove the data access code from your controller and move it into a separate class.
Do not return the technical details of the error, but a user friendly message.
AddAsync only exists for special use cases, all other cases should use the non-async method Add.
From the EF docs:
"This method is async only to allow special value generators, such as
the one used by
'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo',
to access the database asynchronously. For all other cases the non
async method should be used."

I think your code was good, except you don't need to call:
await transaction.RollbackAsync();
in catch block. Failed transaction will auto-rollback when disposed.
Link: https://learn.microsoft.com/en-us/ef/core/saving/transactions

Related

Sync data from remote api and save it to my localdb

I want to sync data from remote api! something like 1M record! but the whole process talks about 5Mins.
as a user experience, that's very bad thing to do! I want the whole process takes less than 1S!
I mainly use .net core web api 6.0 with SQLite, EF Core!
I search a lot and I used BulkInsert! and BlukSaveChangesAsync and same it talks a long time!
Same it's very bad user experience. I tried the following commented solutions and same problem! I want to make it very fast! as the user! does not feel that there is sync in background or thow.
Note: also I stopped all indexes while inserting the data, to make the process faster! and same problem.
Note: My app is Monolithic.
I know I can use something like Azure function but that would be considered as over engineering.
I want the simpliest way to solve this! I searched a lot in YouTube, GitHub and Stack overflow and I found nothing that would help me as I wish.
Note: I'm writing the data in two tables!
first table: contains only 5 rows.
second table: contains 3 rows.
`
public async Task<IEnumerable<DatumEntity>> SyncCities()
{
var httpClient = _httpClientFactory.CreateClient("Cities");
var httpResponseMessage = await httpClient.GetAsync(
"API_KEY_WITH_SOME_CREDS");
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync<Result>(contentStream);
var datums = result!.Data;
if (datums.Any())
{
//First solution
//_context.Datums.AddRange(datums);
//await _context.SaveChangesAsync();
//second solution
//await _context.BulkInsertAsync(datums);
//await _context.BulkSaveChangesAsync();
//Thread solution
//ThreadPool.QueueUserWorkItem(async delegate
//{
// _context.Datums.AddRange(datums);
// await _context.BulkSaveChangesAsync();
//});
}
return datums;
}
return Enumerable.Empty<DatumEntity>();
}
Tried: I tried bulkInsert! tried ThreadPool!stopped all indexes! I tried a lot of things. and nothing helped me as I tought!
I want the whole process takes less than 1S as the user does not move away from the application! because the bad user experience.
This ThreadPool solved the issue for me:
if (datums.Any())
{
ThreadPool.QueueUserWorkItem(async _ =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var context = scope.ServiceProvider
.GetRequiredService<CitiesDbContext>();
context.Datums.AddRange(datums);
await context.SaveChangesAsync();
};
});
}

In Blazor dealing with unexpected results from the api web server

I'm developing a fairly simple Blazor app using a lot of default template functionality, and I can't figure out how to handle an ActionResult from the server that isn't the normal return value.
On the server side I've slightly modified the default controller template to look something like this:
public async Task<ActionResult<MyData>> GetSession(int id)
{
var myData= await FetchMyData(id);
if (myData== null)
{
return NotFound();
}
return myData;
}
That check for a null value was in the original template - it seems like a good idea so I kept it. On the client side my code looks like this:
public async Task<MyData> GetMyData(int id)
{
return await Http.GetJsonAsync<MyData>("/api/MyData/" + id);
}
It all works quite well, except the client side code doesn't handle the case where the server side returns a "NotFound()" result. It's not a show stopper, but it's driving me crazy that I don't know how to do it.
It seems that the GetJsonAsync() call on the client is silently unwrapping the return Value from the ActionResult wrapper (I guess?). Does that mean if I want to handle a NotFound condition I should be using a different httpclient function and maybe deserializing the object Value myself? If so, anyone want to volunteer an example?
Or am I missing something and there's an easier way?
It seems stupid to check for a condition on the server side just to send the client a warning that ultimately results in an unhandled exception.
I tried Henk Holterman's suggestion of just adding a try/catch, and it turns out the exception that was thrown had the information I wanted - that is the status being returned by the server. So what I should have done was this:
public async Task<MyData> GetMyData(int id)
{
try
{
return await Http.GetJsonAsync<MyData>("/api/MyData/" + id);
}
catch (System.Net.Http.HttpRequestException e)
{
.... process the exception
}
}
Turns out HttpRequestException has the HResult, which is what I was looking for.
Thanks Henk.

Getting a client readable message from an Npgsql.PostgresException

I'm writing a web api using PostgreSQL and am checking database constraints as part of the validation process, but I also have a global exception filter as a fallback in case something gets by when saving. My problem is that the exception doesn't seem to have any message that I can present to the client without some processing. The added image is of the PostgresException data from a breakpoint. For example, in this case I would want something along the lines of "Asset Number x already exists" or just "Asset Number must be unique". Is this something that can be configured somewhere? The place that makes the most sense is at the constraint creation code, but I couldn't find an option to do so.
modelBuilder.Entity<AssetItem>().HasIndex(item => new { item.AssetNumber }).IsUnique();
public class DbExceptionFilter : IExceptionFilter
{
private const string UNIQUE_EXCEPTION = "23505";
public async void OnException(ExceptionContext context)
{
var exceptionType = context.Exception.InnerException.GetType().FullName;
if (exceptionType == "Npgsql.PostgresException")
{
var pgException = (PostgresException) context.Exception.InnerException;
switch(pgException.SqlState)
{
case UNIQUE_EXCEPTION:
var error = new {error = "Unique Error Here"};
await WriteJsonErrorResponse(context.HttpContext.Response, HttpStatusCode.BadRequest, error);
return;
}
}
else
{
var error = new { error = "Unexpected Server Error"};
await WriteJsonErrorResponse(context.HttpContext.Response, HttpStatusCode.InternalServerError, error);
return;
}
}
private async Task WriteJsonErrorResponse(HttpResponse response, HttpStatusCode statusCode, dynamic error)
{
response.ContentType = "application/json";
response.StatusCode = (int) statusCode;
await response.Body.WriteAsync(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(error)));
}
}
The closest thing to a user-readable message that PostgreSQL provides is the message text exposed on PostgresException.
However, as a general rule it is not a good idea to expose database errors directly to users (including web API users): these are intended to the application directly interacting with the database (i.e. your application). These messages generally don't mean much to the users of your API, and more importantly they leak potentially sensitive information about your database schema and are therefore not secure. It's especially problematic to dump/serialize the entire exception to the user as you seem to be doing (with JsonConvert.SerializeObject).
The best practice here would be to identify legitimate database exceptions that the user may trigger, intercept these and return and appropriately-worded message of your own (e.g. "A user with that name already exists").
As a side note, to identify PostgresException, rather than getting the name of the exception and comparing to that, you can simply use C# pattern matching:
if (context.Exception.InnerException is PostgresException postgresException)
{
// ...
}

nHibernate session per request and exception management

I have a scenario similar to this:
Asp.NET MVC 4 website using nHibernate Session Per Request.
The session is injected using Ninject onto a Repository with the Get and Save methods.
There are a lot of articles talking about Session Per Request and saying that is the way to do things on a web application.
But i'm having problems implementing logic like this one:
Read Data From Database
Alter Entity information
Save to Database
Read another entity
Alter entity
Save ... but an EXCEPTION OCCURS
I want to show my view with a message to the user. But i have also to refresh the resulting web page,
so i have also to read some information from the database.
According to nHibernate documentation, the session with the exception must be discarded Documentation Here
But i can't find any articles about the best way to proceed here.
What's the best approach for this situation?. I will have to inject a new Session to my repository object?.
Thanks.
You can create a new session from the SessionFactory property of the original session. You can access the original session object by either exposing it in the repository class or injecting it into the controller. Then you can create a new repository with the new session.
I do this in some of my Actions where I expect unique key violations to occur and I have to reload lookup data in the model. Here's an example:
public ActionResult Create(MeasuresEditView model)
{
if (ModelState.IsValid)
{
using (var txn = _session.BeginTransaction())
{
try
{
var measure = new Measure { Code = model.Code };
_session.Save(measure);
txn.Commit();
return RedirectToAction("Index");
}
catch (UniqueKeyException)
{
txn.Rollback();
var msg = string.Format("A measure with the code '{0}' already exists, please enter a different code or cancel.", model.Code);
ModelState.AddModelError("Code", msg);
}
catch (Exception ex)
{
if (txn.IsActive)
{
txn.Rollback();
}
log.Error("Create", ex);
throw;
}
}
}
// have to rebuild selectlist on post in new txn in case it was rolled back
using (var session = _session.SessionFactory.OpenSession())
using (var txn = session.BeginTransaction())
{
SetProductGroupSelectList(session, model, manualId);
txn.Commit();
}
return View(model);
}

NHibernate - Handling StaleObjectStateException to always commit client changes - Need advice/recommendation

I am trying to find the perfect way to handle this exception and force client changes to overwrite any other changes that caused the conflict. The approach that I came up with is to wrap the call to Session.Transaction.Commit() in a loop, inside the loop I would do a try-catch block and handle each stale object individually by copying its properties, except row-version property then refreshing the object to get latest DB data then recopying original values to the refreshed object and then doing a merge. Once I loop I will commit and if any other StaleObjectStateException take place then the same applies. The loop keeps looping until all conflicts are resolved.
This method is part of a UnitOfWork class. To make it clearer I'll post my code:
// 'Client-wins' rules, any conflicts found will always cause client changes to
// overwrite anything else.
public void CommitAndRefresh() {
bool saveFailed;
do {
try {
_session.Transaction.Commit();
_session.BeginTransaction();
saveFailed = false;
} catch (StaleObjectStateException ex) {
saveFailed = true;
// Get the staled object with client changes
var staleObject = _session.Get(ex.EntityName, ex.Identifier);
// Extract the row-version property name
IClassMetadata meta = _sessionFactory.GetClassMetadata(ex.EntityName);
string rowVersionPropertyName = meta.PropertyNames[meta.VersionProperty] as string;
// Store all property values from client changes
var propertyValues = new Dictionary<string, object>();
var publicProperties = staleObject.GetType().GetProperties();
foreach (var p in publicProperties) {
if (p.Name != rowVersionPropertyName) {
propertyValues.Add(p.Name, p.GetValue(staleObject, null));
}
}
// Get latest data for staled object from the database
_session.Refresh(staleObject);
// Update the data with the original client changes except for row-version
foreach (var p in publicProperties) {
if (p.Name != rowVersionPropertyName) {
p.SetValue(staleObject, propertyValues[p.Name], null);
}
}
// Merge
_session.Merge(staleObject);
}
} while (saveFailed);
}
The above code works fine and handle concurrency with the client-wins rule. However, I was wondering if there is any built-in capabilities in NHibernate to do this for me or if there is a better way to handle this.
Thanks in advance,
What you're describing is a lack of concurrency checking. If you don't use a concurrency strategy (optimistic-lock, version or pessimistic), StaleStateObjectException will not be thrown and the update will be issued.
Okay, now I understand your use case. One important point is that the ISession should be discarded after an exception is thrown. You can use ISession.Merge to merge changes between a detached a persistent object rather than doing it yourself. Unfortunately, Merge does not cascade to child objects so you still need to walk the object graph yourself. So the implementation would look something like:
catch (StaleObjectStateException ex)
{
if (isPowerUser)
{
var newSession = GetSession();
// Merge will automatically get first
newSession.Merge(staleObject);
newSession.Flush();
}
}