Ho una classe Tools_NawContext
estende DbContext
e una classe DbResult
per modificare un po 'il risultato del metodo SaveChanges
quando si verifica un'eccezione. Quando viene lanciata un'eccezione, creo un messaggio di errore specifico che conosco appartenente all'entità che cerco di aggiungere, eliminare o modificare. L'utente può intraprendere azioni appropriate in base al messaggio di errore e riprovare.
public partial class Tools_NawContext : DbContext
{
public Tools_NawContext(DbContextOptions<Tools_NawContext> options) : base(options) { }
public DbResult TrySaveChanges()
{
try {
int numberOfRowsSaved = SaveChanges();
return new DbResult(numberOfRowsSaved);
} catch(Exception ex) {
return new DbResult(ex);
}
}
}
public class DbResult
{
public DbResult(int numberOfRowsSaved) {
this.Succeeded = true;
this.NumberOfRowsSaved = numberOfRowsSaved;
}
public DbResult(Exception exception)
{
this.Exception = exception;
if(exception.GetType() == typeof(DbUpdateException) && exception.InnerException != null) {
if (exception.InnerException.Message.StartsWith("The DELETE statement conflicted with the REFERENCE constraint")) {
this.DuplicateKeyError = true;
this.DuplicateKeyErrorMessage = "There are other objects related to this object. First delete all the related objects.";
} else if (exception.InnerException.Message.StartsWith("Violation of PRIMARY KEY constraint")) {
this.DuplicateKeyError = true;
this.DuplicateKeyErrorMessage = "There is already a row with this key in the database.";
} else if (exception.InnerException.Message.StartsWith("Violation of UNIQUE KEY constraint")) {
this.DuplicateKeyError = true;
this.DuplicateKeyErrorMessage = "There is already a row with this key in the database.";
}
} else if(exception.GetType() == typeof(System.InvalidOperationException) && exception.Message.StartsWith("The association between entity types")) {
this.DuplicateKeyError = true;
this.DuplicateKeyErrorMessage = "There are other objects related to this object. First delete all the related objects.";
}
}
public bool Succeeded { get; private set; }
public int NumberOfRowsSaved { get; private set; }
public bool DuplicateKeyError { get; private set; }
public string DuplicateKeyErrorMessage { get; private set; }
public Exception Exception { get; private set; }
public List<string> ErrorMessages { get; set; }
public string DefaultErrorMessage { get { if (Succeeded == false) return "Er is een fout in de database opgetreden."; else return ""; } private set { } }
}
Tuttavia ora sto provando ad importare alcuni JSon e voglio usare ancora il metodo TrySaveChanges
. Tuttavia questa volta dopo alcuni controlli, prima aggiungo più entità al contesto, non solo 1. Una volta aggiunto tutto, chiamo il metodo TrySaveChanges
. Funziona ancora ma se qualcosa non funziona non riesco a determinare quali entità non sono state salvate. Se aggiungo 1000 entità e solo 1 fallirà non riesco a determinare dove è andato storto. Come posso determinare quali entità aggiunte stanno generando errori? Di seguito è riportato un esempio di come lo utilizzo.
Ho 2 classi generate da EF. Testresultaten
e Keuring
public partial class Testresultaten
{
public int KeuringId { get; set; }
public int TestId { get; set; }
public string Resultaat { get; set; }
public string Status { get; set; }
public int TestinstrumentId { get; set; }
public virtual Keuring Keuring { get; set; }
public virtual Test Test { get; set; }
public virtual Testinstrument Testinstrument { get; set; }
}
public partial class Keuring
{
public Keuring()
{
Keuring2Werkcode = new HashSet<Keuring2Werkcode>();
Testresultaten = new HashSet<Testresultaten>();
}
public int Id { get; set; }//NOTE: Auto-incremented by DB!
public int GereedschapId { get; set; }
public DateTime GekeurdOp { get; set; }
public int KeuringstatusId { get; set; }
public int TestmethodeId { get; set; }
public DateTime GekeurdTot { get; set; }
public string GekeurdDoor { get; set; }
public string Notitie { get; set; }
public virtual ICollection<Keuring2Werkcode> Keuring2Werkcode { get; set; }
public virtual ICollection<Testresultaten> Testresultaten { get; set; }
public virtual Gereedschap Gereedschap { get; set; }
public virtual Keuringstatus Keuringstatus { get; set; }
public virtual Testmethode Testmethode { get; set; }
}
Ho una classe _KeuringImporter
che ha un metodo che aggiunge un nuovo newKeuring
e un testresultatenList
al dbContext ( _Tools_NawContext
).
private Result<KeuringRegel, Keuring> SetupKeuringToDB2(KeuringRegel row, int rownr, Keuring newKeuring)
{
_Tools_NawContext.Keuring.Add(newKeuring);
List<string> errorMessages = new List<string>();
List<Testresultaten> testresultatenList = new List<Testresultaten>();
foreach (string testName in row.testNames.Keys.ToList())
{
string testValue = row.testNames[testName].ToString();
Test test = _Tools_NawContext.Test.Include(item => item.Test2Testmethode).SingleOrDefault(item => item.Naam.Equals(testName, StringComparison.OrdinalIgnoreCase));
//-----!!NOTE!!-----: Here KeuringId = newKeuring.Id is a random negative nr and is not beeing roundtriped to the db yet!
Testresultaten newTestresultaten = new Testresultaten() { KeuringId = newKeuring.Id, TestId = test.Id, Resultaat = testValue, Status = row.Status, TestinstrumentId = 1 };
testresultatenList.Add(newTestresultaten);
}
_Tools_NawContext.Testresultaten.AddRange(testresultatenList);
return new Result<KeuringRegel, Keuring>(row, newKeuring, errorMessages);
}
Come ho detto. Lo uso per importare JSON. Se un file JSON contiene 68 righe, il metodo viene chiamato 68 volte. O per parlare: 68 nuovi articoli Keuring
sono allegati a DbContext e anche ogni volta che un elenco di Testresultaten
viene aggiunto a DbContext.
Una volta che tutto è configurato, chiamo finalmente SaveSetupImportToDB
dal mio controller. (Questo metodo è anche parte della mia classe _KeuringImporter
.)
public DbResult SaveSetupImportToDB()
{
DbResult dbResult = _Tools_NawContext.TrySaveChanges();
return dbResult;
}
Come ottengo ciò che voglio? Nel caso precedente nel mio database MS SQL, la tabella Keuring
ha una chiave primaria di Id
che viene incrementata automaticamente dal db. La tabella ha anche una chiave univoca combinata di GereedschapId
e GekeurdOp
.
Posso scrivere alcuni assegni prima che un nuovo newKeuring
sia aggiunto al contesto, come questo:
private Result<KeuringRegel, Keuring> SetupKeuringToDB2(KeuringRegel row, int rownr, Keuring newKeuring)
{
List<string> errorMessages = new List<string>();
var existingKeuring = _Tools_NawContext.Keuring.SingleOrDefault(x => x.Id == newKeuring.Id);
if(existingKeuring == null) { errorMessages.Add("There is already a keuring with id " + newKeuring.Id + " in the db."); }
existingKeuring = _Tools_NawContext.Keuring.SingleOrDefault(x => x.GereedschapId == newKeuring.GereedschapId && x.GekeurdOp == newKeuring.GekeurdOp);
if (existingKeuring == null) { errorMessages.Add("There is already a keuring with GereedschapId " + newKeuring.GereedschapId + " and GekeurdOp " + newKeuring.GekeurdOp + " in the db."); }
//Some more checks to cerrect values of properties:
//-DateTimes are not in future
//-Integers beeing greater then zero
//-String lengths not beeing larger then 500 characters
//-And so on, etc...
_Tools_NawContext.Keuring.Add(newKeuring);
List<Testresultaten> testresultatenList = new List<Testresultaten>();
foreach (string testName in row.testNames.Keys.ToList())
{
string testValue = row.testNames[testName].ToString();
Test test = _Tools_NawContext.Test.Include(item => item.Test2Testmethode).SingleOrDefault(item => item.Naam.Equals(testName, StringComparison.OrdinalIgnoreCase));
//-----!!NOTE!!-----: Here KeuringId = newKeuring.Id is a random negative nr and is not beeing roundtriped to the db yet!
Testresultaten newTestresultaten = new Testresultaten() { KeuringId = newKeuring.Id, TestId = test.Id, Resultaat = testValue, Status = row.Status, TestinstrumentId = 1 };
testresultatenList.Add(newTestresultaten);
}
_Tools_NawContext.Testresultaten.AddRange(testresultatenList);
return new Result<KeuringRegel, Keuring>(row, newKeuring, errorMessages);
}
I primi controlli aggiunti sono semplici controlli per vedere se un elemento esiste già nel db. Dovrò fare questi controlli per ogni entità che aggiungo al db. Preferisco semplicemente aggiungerli senza controlli, catturare l'eccezione quando si chiama SaveChanges
e dire all'utente cosa è andato storto. Mi fa risparmiare un sacco di controllo attraverso la mia applicazione. So che non riesco a controllare per ogni situazione e questo è il motivo per cui la classe DbResult
ha anche la proprietà DefaultErrorMessage
. Funziona tutto bene se "crudo" 1 entità al momento. Il problema inizia quando si aggiungono più entità contemporaneamente. Qualche suggerimento su come posso migliorare il mio codice in modo da poter scoprire dove qualcosa è andato storto? Idealmente dopo aver chiamato SaveChanges()
. Ma ogni altra idea è benvenuta! Forse cambiando una proprietà sul DbContext
che controlla se un'entità esiste già se viene aggiunta ai contesti.
Nel caso in cui chiami SaveChanges
e fallirà il rollback di tutte le azioni nel batch. Inoltre, riceverai una DbUpdateException
con le proprietà Entries
che contengono voci / voci che causano un errore. Il contesto stesso salverà comunque lo stato degli oggetti tracciati ( incluso il fallimento ) che puoi ottenere usando ChangeTracker.Entries()
(probabilmente non ti servirà)
try
{
model.SaveChanges();
}
catch (DbUpdateException e)
{
//model.ChangeTracker.Entries();
//e.Entries - Resolve errors and try again
}
Nel tuo caso, puoi creare un ciclo che continuerà i tentativi fino a quando non verrà salvato qualcosa come
while (true)
{
try
{
model.SaveChanges();
break;
}
catch (DbUpdateException e)
{
foreach (var entry in e.Entries)
{
// Do some logic or fix
// or just detach
entry.State = System.Data.Entity.EntityState.Detached;
}
}
}