L'une de mes tables a une clé unique et lorsque j'essaie d'insérer un enregistrement en double, une exception est levée comme prévu. Mais je dois distinguer les exceptions de clés uniques des autres, afin de pouvoir personnaliser le message d'erreur pour les violations de contraintes de clé uniques.
Toutes les solutions que j'ai trouvées en ligne suggèrent de lancer ex.InnerException
à System.Data.SqlClient.SqlException
et vérifiez si la propriété if Number
est égale à 2601 ou 2627 comme suit:
try
{
_context.SaveChanges();
}
catch (Exception ex)
{
var sqlException = ex.InnerException as System.Data.SqlClient.SqlException;
if (sqlException.Number == 2601 || sqlException.Number == 2627)
{
ErrorMessage = "Cannot insert duplicate values.";
}
else
{
ErrorMessage = "Error while saving data.";
}
}
Mais le problème est, casting ex.InnerException
à System.Data.SqlClient.SqlException
provoque une erreur de conversion invalide puisque ex.InnerException
est en fait le type de System.Data.Entity.Core.UpdateException
, ne pas System.Data.SqlClient.SqlException
.
Quel est le problème avec le code ci-dessus? Comment identifier les violations de contrainte de clé unique?
Avec EF6 et l'API DbContext
(pour SQL Server), j'utilise actuellement ce morceau de code:
try
{
// Some DB access
}
catch (Exception ex)
{
HandleException(ex);
}
public virtual void HandleException(Exception exception)
{
if (exception is DbUpdateConcurrencyException concurrencyEx)
{
// A custom exception of yours for concurrency issues
throw new ConcurrencyException();
}
else if (exception is DbUpdateException dbUpdateEx)
{
if (dbUpdateEx.InnerException != null
&& dbUpdateEx.InnerException.InnerException != null)
{
if (dbUpdateEx.InnerException.InnerException is SqlException sqlException)
{
switch (sqlException.Number)
{
case 2627: // Unique constraint error
case 547: // Constraint check violation
case 2601: // Duplicated key row error
// Constraint violation exception
// A custom exception of yours for concurrency issues
throw new ConcurrencyException();
default:
// A custom exception of yours for other DB issues
throw new DatabaseAccessException(
dbUpdateEx.Message, dbUpdateEx.InnerException);
}
}
throw new DatabaseAccessException(dbUpdateEx.Message, dbUpdateEx.InnerException);
}
}
// If we're here then no exception has been thrown
// So add another piece of code below for other exceptions not yet handled...
}
Comme vous l'avez mentionné UpdateException
, je suppose que vous utilisez l'API ObjectContext
, mais elle devrait être similaire.
Dans mon cas, j'utilise EF 6 et j'ai décoré l'une des propriétés de mon modèle avec:
[Index(IsUnique = true)]
Pour attraper la violation, je fais ce qui suit en utilisant C # 7, cela devient beaucoup plus facile:
protected async Task<IActionResult> PostItem(Item item)
{
_DbContext.Items.Add(item);
try
{
await _DbContext.SaveChangesAsync();
}
catch (DbUpdateException e)
when (e.InnerException?.InnerException is SqlException sqlEx &&
(sqlEx.Number == 2601 || sqlEx.Number == 2627))
{
return StatusCode(StatusCodes.Status409Conflict);
}
return Ok();
}
Notez que cela ne détectera que la violation de contrainte d'index unique.
// put this block in your loop
try
{
// do your insert
}
catch(SqlException ex)
{
// the exception alone won't tell you why it failed...
if(ex.Number == 2627) // <-- but this will
{
//Violation of primary key. Handle Exception
}
}
MODIFIER:
Vous pouvez également simplement inspecter le composant message de l'exception. Quelque chose comme ça:
if (ex.Message.Contains("UniqueConstraint")) // do stuff
Si vous voulez attraper une contrainte unique
try {
// code here
}
catch(Exception ex) {
//check for Exception type as sql Exception
if(ex.GetBaseException().GetType() == typeof(SqlException)) {
//Violation of primary key/Unique constraint can be handled here. Also you may //check if Exception Message contains the constraint Name
}
}
try
{
// do your insert
}
catch(Exception ex)
{
if (ex.GetBaseException().GetType() == typeof(SqlException))
{
Int32 ErrorCode = ((SqlException)ex.InnerException).Number;
switch(ErrorCode)
{
case 2627: // Unique constraint error
break;
case 547: // Constraint check violation
break;
case 2601: // Duplicated key row error
break;
default:
break;
}
}
else
{
// handle normal exception
}
}
J'ai pensé qu'il pourrait être utile de montrer du code non seulement en gérant l'exception de lignes en double, mais également en extrayant des informations utiles pouvant être utilisées à des fins de programmation. Par exemple. composer un message personnalisé.
Cette sous-classe Exception
utilise regex pour extraire le nom de la table de base de données, le nom de l'index et les valeurs de clé.
public class DuplicateKeyRowException : Exception
{
public string TableName { get; }
public string IndexName { get; }
public string KeyValues { get; }
public DuplicateKeyRowException(SqlException e) : base(e.Message, e)
{
if (e.Number != 2601)
throw new ArgumentException("SqlException is not a duplicate key row exception", e);
var regex = @"\ACannot insert duplicate key row in object \'(?<TableName>.+?)\' with unique index \'(?<IndexName>.+?)\'\. The duplicate key value is \((?<KeyValues>.+?)\)";
var match = new System.Text.RegularExpressions.Regex(regex, System.Text.RegularExpressions.RegexOptions.Compiled).Match(e.Message);
Data["TableName"] = TableName = match?.Groups["TableName"].Value;
Data["IndexName"] = IndexName = match?.Groups["IndexName"].Value;
Data["KeyValues"] = KeyValues = match?.Groups["KeyValues"].Value;
}
}
La classe DuplicateKeyRowException
est assez facile à utiliser ... créez simplement un code de traitement des erreurs comme dans les réponses précédentes ...
public void SomeDbWork() {
// ... code to create/edit/update/delete entities goes here ...
try { Context.SaveChanges(); }
catch (DbUpdateException e) { throw HandleDbUpdateException(e); }
}
public Exception HandleDbUpdateException(DbUpdateException e)
{
// handle specific inner exceptions...
if (e.InnerException is System.Data.SqlClient.SqlException ie)
return HandleSqlException(ie);
return e; // or, return the generic error
}
public Exception HandleSqlException(System.Data.SqlClient.SqlException e)
{
// handle specific error codes...
if (e.Number == 2601) return new DuplicateKeyRowException(e);
return e; // or, return the generic error
}