mvc - crud entity framework asp net c#
OptimisticConcurrencyException no funciona en Entity Framework en ciertas situaciones (4)
ACTUALIZACIÓN (2010-12-21): Reescribió completamente esta pregunta en base a las pruebas que he estado haciendo. Además, esto solía ser una pregunta específica de POCO, pero resulta que mi pregunta no es necesariamente específica de POCO.
Estoy usando Entity Framework y tengo una columna de marca de tiempo en mi tabla de base de datos que debería usarse para rastrear los cambios para una concurrencia optimista. He establecido el modo de concurrencia para esta propiedad en el Diseñador de entidades en "Solucionado" y obtengo resultados inconsistentes. Aquí hay un par de escenarios simplificados que demuestran que la comprobación de concurrencia funciona en un escenario pero no en otro.
Lanza con éxito la excepción OptimisticConcurrencyException:
Si adjunto una entidad desconectada, SaveChanges lanzará una excepción OptimisticConcurrencyException si hay un conflicto de marca de tiempo:
[HttpPost]
public ActionResult Index(Person person) {
_context.People.Attach(person);
var state = _context.ObjectStateManager.GetObjectStateEntry(person);
state.ChangeState(System.Data.EntityState.Modified);
_context.SaveChanges();
return RedirectToAction("Index");
}
No lanza la excepción OptimisticConcurrencyException:
Por otro lado, si recupero una nueva copia de mi entidad de la base de datos y hago una actualización parcial en algunos campos, y luego llamo a SaveChanges (), entonces aunque haya un conflicto de marca de tiempo, no obtengo una excepción OptimisticConcurrencyException :
[HttpPost]
public ActionResult Index(Person person) {
var currentPerson = _context.People.Where(x => x.Id == person.Id).First();
currentPerson.Name = person.Name;
// currentPerson.VerColm == [0,0,0,0,0,0,15,167]
// person.VerColm == [0,0,0,0,0,0,15,166]
currentPerson.VerColm = person.VerColm;
// in POCO, currentPerson.VerColm == [0,0,0,0,0,0,15,166]
// in non-POCO, currentPerson.VerColm doesn''t change and is still [0,0,0,0,0,0,15,167]
_context.SaveChanges();
return RedirectToAction("Index");
}
Basado en el Analizador de SQL, parece que Entity Framework está ignorando el nuevo VerColm (que es la propiedad de marca de tiempo) y en su lugar utiliza el VerColm cargado originalmente. Debido a esto, nunca lanzará una excepción OptimisticConcurrencyException.
ACTUALIZACIÓN: Agregar información adicional por solicitud de enero:
Tenga en cuenta que también agregué comentarios al código anterior para coincidir con lo que veo en la acción de mi controlador mientras analizo este ejemplo.
Este es el valor de VerColm en mi base de datos antes de la actualización: 0x0000000000000FA7
Esto es lo que muestra el Analizador de SQL al realizar la actualización:
exec sp_executesql N''update [dbo].[People]
set [Name] = @0
where (([Id] = @1) and ([VerColm] = @2))
select [VerColm]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = @1'',N''@0 nvarchar(50),@1 int,@2 binary(8)'',@0=N''hello'',@1=1,@2=0x0000000000000FA7
Tenga en cuenta que @ 2 debería haber sido 0x0000000000000FA6, pero es 0x0000000000000FA7
Aquí está el VerColm en mi base de datos después de la actualización: 0x0000000000000FA8
¿Alguien sabe cómo puedo solucionar este problema? Me gustaría que Entity Framework lance una excepción cuando actualizo una entidad existente y hay un conflicto de marca de tiempo.
Gracias
Aquí hay otro enfoque que es un poco más genérico y se ajusta a la capa de datos:
// if any timestamps have changed, throw concurrency exception
var changed = this.ChangeTracker.Entries<>()
.Any(x => !x.CurrentValues.GetValue<byte[]>("Timestamp").SequenceEqual(
x.OriginalValues.GetValue<byte[]>("Timestamp")));
if (changed) throw new OptimisticConcurrencyException();
this.SaveChanges();
Simplemente verifica si TimeStamp ha cambiado y emite una excepción de concurrencia.
He modificado la solución @JarrettV para que funcione con Entity Framework Core. En este momento, está iterando a través de todas las entradas modificadas en contexto y buscando cualquier discrepancia en la propiedad marcada como token de concurrencia. También funciona para TimeStamp (RowVersion):
private void ThrowIfInvalidConcurrencyToken()
{
foreach (var entry in _context.ChangeTracker.Entries())
{
if (entry.State == EntityState.Unchanged) continue;
foreach (var entryProperty in entry.Properties)
{
if (!entryProperty.IsModified || !entryProperty.Metadata.IsConcurrencyToken) continue;
if (entryProperty.OriginalValue != entryProperty.CurrentValue)
{
throw new DbUpdateConcurrencyException(
$"Entity {entry.Metadata.Name} has been modified by another process",
new List<IUpdateEntry>()
{
entry.GetInfrastructure()
});
}
}
}
}
Y solo necesitamos invocar este método antes de guardar los cambios en el contexto de EF:
public async Task SaveChangesAsync(CancellationToken cancellationToken)
{
ThrowIfInvalidConcurrencyToken();
await _context.SaveChangesAsync(cancellationToken);
}
Si es primero el Código EF, entonces use un código similar al código que se encuentra debajo. Esto cambiará el TimeStamp original cargado de db al de la interfaz de usuario y asegurará que se produzca OptimisticConcurrencyEception
.
db.Entry(request).OriginalValues["Timestamp"] = TimeStamp;
Explicación
La razón por la que no obtiene la excepción OptimisticConcurrencyException
en su segundo ejemplo de código se debe a la manera en que EF verifica la concurrencia:
Cuando recupera entidades al consultar su base de datos, EF recuerda el valor de todas las propiedades marcadas con ConcurrencyMode.Fixed
en el momento de la consulta como los valores originales no modificados.
Luego cambia algunas propiedades (incluidas las que están marcadas como Fixed
) y llama a SaveChanges()
en su DataContext.
EF comprueba las actualizaciones simultáneas comparando los valores actuales de todas las columnas de BD marcadas Fixed
con los valores no modificados originales de las propiedades marcadas Fixed
. El punto clave aquí es que EF trata la actualización de su propiedad de marca de tiempo como una actualización de propiedad de datos normal . El comportamiento que ves es por diseño.
Solución / Solución
Para solucionarlo tienes las siguientes opciones:
Utilice su primer enfoque: no vuelva a consultar la base de datos de su entidad, sino adjunte la entidad recreada a su contexto.
Falsifique su valor de marca de tiempo para que sea el valor de db actual, de modo que la verificación de concurrencia de EF use el valor suministrado como se muestra a continuación (vea también esta respuesta en una pregunta similar):
var currentPerson = _context.People.Where(x => x.Id == person.Id).First(); currentPerson.VerColm = person.VerColm; // set timestamp value var ose = _context.ObjectStateManager.GetObjectStateEntry(currentPerson); ose.AcceptChanges(); // pretend object is unchanged currentPerson.Name = person.Name; // assign other data properties _context.SaveChanges();
Puede verificar la concurrencia usted mismo comparando su valor de marca de hora con el valor de marca de hora requerido:
var currentPerson = _context.People.Where(x => x.Id == person.Id).First(); if (currentPerson.VerColm != person.VerColm) { throw new OptimisticConcurrencyException(); } currentPerson.Name = person.Name; // assign other data properties _context.SaveChanges();