So I've got this piece of code which, logically should work but Entity Framework is behaving unexpectedly.
Here:
foreach (SomeClass someobject in allObjects)
{
Supplier supplier = new Supplier();
supplier.primary_key = someobject.id;
supplier.name = someobject.displayname;
try
{
sm.Add(supplier);
ro.Created++;
}
catch (Exception ex)
{
ro.Error++;
}
}
Here's what I have in sm.Add()
public Supplier Add(Supplier supplier)
{
try
{
_ctx.AddToSupplier(supplier);
_ctx.SaveChanges();
return supplier;
}
catch (Exception ex)
{
throw;
}
}
I can have records in allObjects that have the same id. My piece of code needs to support this and just move on to the next and try to insert it, which I think should work.
If this happens, an exception is throw, saying that records with dupe PKs cannot be inserted (of course). The exception mentions the value of the PK, for example 1000.
All is well, a new supplier is passed to sm.Add() containing a PK that's never been used before. (1001)
Weirdly though, when doing SaveChanges(), EF will whine about not being able to insert records with dupe PKs. The exception still mentions 1000 even though supplier contains 1001 in primary_key.
I feel this is me not using _ctx properly. Do I need to call something else to sync it ?
Found it, had to change something in the Add() method:
public Supplier Add(Supplier supplier)
{
try
{
_ctx.AddToSupplier(supplier);
_ctx.SaveChanges();
return supplier;
}
catch (Exception ex)
{
_ctx.Supplier.Detach(supplier);
throw;
}
}
Related
Is it possible to skip invalid values when I'm saving an entity with SaveChanges and save only valid fields?
It's pretty simple to skip on entities level, but I'm not able to find a way of doing that on fields level.
Muhammad's answer gave me great idea. The solution is pretty simple:
try
{
context.Entry(objInDB).State = EntityState.Modified;
context.SaveChanges();
}
catch (Exception ex)
{
var exception = ex as DbEntityValidationException;
if (exception != null)
{
exception.EntityValidationErrors.ToList().ForEach(error =>
{
error.ValidationErrors.ToList().ForEach(validationError =>
{
error.Entry.Property(validationError.PropertyName).IsModified = false;
});
});
context.SaveChanges();
}
}
I have a code like this:
try
{
Member member = database.Members.Where(m=>m.ID=1).FirstOrDefault();
member.Name = "NewMemberName";
database.Entry(member).State = EntityState.Modified;
database.SaveChanges();
}
catch (Exception ex)
{
database.Logs.Add(new Log() { Value=ex.ToString() });
database.SaveChanges();
}
And Entity:
[StringLength(5)]
public string Name { get; set; }
If the Name String more than 5 it would be error and catch the exception ,but when I add a log then save ,the exception from SaveChange(); still remains,how should I do?(Can't change the schema)
the exception from SaveChange(); still remains
Well, if this throws an exception:
database.SaveChanges();
Then there's a pretty good chance that this will also throw an exception:
database.SaveChanges();
Basically, in your catch block you shouldn't be immediately re-trying the operation that just failed a millisecond ago. Instead, log the failure and handle the exception:
catch (Exception ex)
{
// DO NOT call SaveChanges() here.
}
Of course, if writing to the database is failing, then logging to the database is also likely to fail. Suppose for example that the connection string is wrong or the database is down or timing out. You can't log that.
I recommend using a logging framework (log4net, NLog, etc.) as a separate dependency from your Entity Framework data access layer. It's a small learning curve, but you end up with a pretty robust logging system that can much more effectively handle problems. And can be easily configured to log to multiple places, so if writing to one error log (the database) fails then you still have another one (a file, for example).
At the very least, if persisting your data context fails, you'll need to log to a new data context. Otherwise the part that failed is still there.
Something structurally more like this:
try
{
using (var database = new DbContext())
{
Member member = database.Members.Where(m=>m.ID=1).FirstOrDefault();
member.Name = "NewMemberName";
database.Entry(member).State = EntityState.Modified;
database.SaveChanges();
}
}
catch (Exception ex)
{
using (var database = new DbContext())
{
database.Logs.Add(new Log() { Value=ex.ToString() });
database.SaveChanges();
}
}
I am using Entity Framework so I believe I should catch an NpgsqlException since it's the .NET data provider for PostgreSQL. So let's say I make a query to the context. If the table doesn't exist in the PostgreSQL database, I want to catch the exception thrown and then manually create it. The code below is an example of how an entity is inserted, and I have attempted to use error handling to create the table if need be:
try
{
return _context.Set(entityType).Add(entity);
}
catch (NpgsqlException)
{
CreateEntityTable(entity); //a private method I made
return _context.Set(entityType).Add(entity);
}
The problems are:
I'm not 100% sure I should be catching an NpgsqlException
I want to be sure that if an exception is thrown, it's because the table doesn't exist. I looked up the PostgreSQL documentation of error codes and error code 42P01 is undefined table. I believe I want to use that, but how? I looked up the members of the NpgsqlException class, and I found ErrorCode. However, that's a type int. It would be nice if I could change the code above to be like the following
try
{
return _context.Set(entityType).Add(entity);
}
catch (NpgsqlException ex)
{
if (ex.ErrorCode.Equals(42P01))
{
CreateEntityTable(entity); //a private method I made
return _context.Set(entityType).Add(entity);
}
}
But I'm not sure if that makes sense (I'm not even sure how 42P01 can be an int).
Any help would be appreciated.
You will want to use the Code property of the NpgsqlException as that will contain PostgreSql error code.
Updating your example:
try
{
return _context.Set(entityType).Add(entity);
}
catch (NpgsqlException ex)
{
if (ex.Code == "42P01")
{
CreateEntityTable(entity); //a private method I made
return _context.Set(entityType).Add(entity);
}
}
As an aside, I would suggest that you don't perform schema updates in your normal code. Only do something of this nature in an installer, or on start up as an upgrade.
Tested and working with ASP NET Core 3.1 and ASP NET Core 5.0
try
{
_context.Add(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException ex)
{
if (ex.GetBaseException() is PostgresException pgException)
{
switch (pgException.SqlState)
{
case "23505":
ModelState.AddModelError(string.Empty, "This entity exists in the database");
return View(yourViewModelFromRequest);
default:
throw;
}
}
}
Try
...
catch (NpgsqlException e)
{
switch (e.SqlState)
{
case "23505":
MessageBox.Show("Some message...");
break;
default:
MessageBox.Show("Some message. Details: " + e.Message);
break;
}
}
At work we are looking to move to an ORM (still using an access database with ADO!) I started building with entity framework and everything was going smoothly until I separated it off into it's own .dll (so we could have the website/crm/production/barcoding systems all using the same database logic).
The issue comes with handling the DbEntityValidationExceptions, my initial test code (which worked)
public override int SaveChanges(System.Data.Objects.SaveOptions options)
{
try{return base.SaveChanges(options);}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
Trace.TraceInformation("Class: {0}, Property: {1}, Error: {2}",
validationErrors.Entry.Entity.GetType().FullName,
validationError.PropertyName,
validationError.ErrorMessage);
}
}
//handle here
throw;
}
}
but this doesn't get caught now and i'm left handling a generic threadException. Is there any way to access the original data (cast the threadException?) or is there a different approach I should take. I think I just need a push in the right direction and I can figure something out.
Regards, Pete
UPDATE:
Hmm bit of an issue calling the GetValidationErrors. I notice that my database Context has the baseClass of ObjectContext and not DbContext (So I can't call the ValidateEntity). I'm using Entity Framework 5 with default code generation enabled - using a database first approach if that helps.
check for validation errors before calling SaveChanges. Something like...
var errors = context.GetValidationErrors();
if(errors.Any())
{
//handle validation errors
}
else
{
context.SaveChanges();
}
from memory, so the exact syntax may not be correct.
this is what I ended up going with in the end (i'll probably end up fleshing it out as I learn more about EF)
public List<DbEntityValidationException> vErrors = new List<DbEntityValidationException>();
public int DbChanges = 0;
public bool SaveChanges()
{
try
{
this.vErrors = (List<DbEntityValidationException>)base.GetValidationErrors();
if (this.vErrors.Count == 0)
{
this.DbChanges = base.SaveChanges();
return true;
}
}
catch (Exception Ex)
{
this.vErrors.Add(new DbEntityValidationException(string.Format("General Error: {0}", Ex.GetType().ToString())));
}
return false;
}
and from code
using(Db db = new Db())
{
//changes
if(db.SaveChanges)
{
//some message using db.DbChanges
}
else
{
//handle errors in db.vErrors
}
Please see to example1. If some of the data will be entered incorrectly, EF4 will not survive nor any record.
The question: whether as a force to ignore an error in one record and continue on.
example1:
foreach (var tag in split)
{
context.NameToResourcer.AddObject(new NameToResourcer()
{
id_resource = resource.id,
name = tag
});
}
context.NameToResourcer.AddObject(new NameToResourcer()
{
id_resource = resource.id,
name = ExtractDomainNameFromURL(resource.url)
});
try
{
context.SaveChanges();
}
catch (UpdateException ex)
{
}
catch (Exception ex)
{
throw;
}
example2 alternative:
foreach (var tag in split)
{
try
{
context.NameToResourcer.AddObject(new NameToResourcer()
{
id_resource = resource.id,
name = tag
});
context.SaveChanges();
}
catch (UpdateException ex)
{
}
}
try
{
context.NameToResourcer.AddObject(new NameToResourcer()
{
id_resource = resource.id,
name = ExtractDomainNameFromURL(resource.url)
});
context.SaveChanges();
}
catch (UpdateException ex)
{
}
Context behaves like unit of work. It means that when you modify data and store them with the single call to SaveChanges you are telling EF that you want atomic operation - either all changes are successfully saved or all changes are rolled back. EF use a transaction internally to support this behavior. If you don't want this behavior you cannot save all data with single call to SaveChanges. You must use separate call for each atomic set of data.
One possible solution is to disable validation on saving.But I don't recommend it.
db.Configuration.ValidateOnSaveEnabled = false;