EF Core: establecer la marca de tiempo antes de guardar aún utiliza el valor antiguo

entity-framework entity-framework-core

Pregunta

Tengo un modelo con una columna de marca de tiempo (token de concurrencia). Estoy tratando de escribir una prueba de integración en la que verifico que funciona como espero pero sin éxito. Mi prueba se parece a la siguiente

  1. Obtenga la entidad con la que debe actualizarse desde la API web con una llamada HttpClient.
  2. Haga una solicitud directamente al Contexto y obtenga la misma entidad
  3. Cambie una propiedad en la entidad desde el paso 2.
  4. Guardar la entidad actualizada en el paso 3.
  5. Cambie una propiedad en la entidad desde el paso 1.
  6. Envíe una solicitud de venta con la nueva entidad con HttpClient a la Web Api.
  7. En mi API web, primero obtengo la entidad de la base de datos, establece la propiedad y el valor de la marca de tiempo del que obtuve del cliente. Ahora mi objeto de entidad en el controlador api tiene un valor de marca de tiempo diferente al de la base de datos. Ahora espero que las salvaciones falle, pero no lo hace. En su lugar, guarda la entidad en la base de datos y genera un nuevo valor de marca de tiempo. Verifiqué con Sql Server Profiler para ver la consulta generada y resulta que todavía se usa el valor de marca de tiempo anterior y no el que asigné a la entidad en mi controlador api.

¿Cuál es la razón de esto? ¿Tiene algo que ver con que Timestamp sea un valor generado por la base de datos que hace que EF ignore los cambios realizados desde la capa empresarial?

La aplicación de prueba completa se puede encontrar aquí: https://github.com/Abrissirba/EfTimestampBug

    public class BaseModel
    {
        [Timestamp]
        public byte[] Timestamp { get; set; }
    }

    public class Person : BaseModel
    {
        public int Id { get; set; }

        public String Title { get; set; }
    }

    public class Context : DbContext
    {
        public Context()
        {}

        public Context(DbContextOptions options) : base(options)
        {}

        public DbSet<Person> Persons{ get; set; }
    }

    protected override void BuildModel(ModelBuilder modelBuilder)
    {
        modelBuilder
            .HasAnnotation("ProductVersion", "7.0.0-rc1-16348")
            .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

        modelBuilder.Entity("EFTimestampBug.Models.Person", b =>
            {
                b.Property<int>("Id")
                    .ValueGeneratedOnAdd();

                b.Property<byte[]>("Timestamp")
                    .IsConcurrencyToken()
                    .ValueGeneratedOnAddOrUpdate();

                b.Property<string>("Title");

                b.HasKey("Id");
            });
    }

    // PUT api/values/5
    [HttpPut("{id}")]
    public Person Put(int id, [FromBody]Person personDTO)
    {
        // 7
        var person = db.Persons.SingleOrDefault(x => x.Id == id);
        person.Title = personDTO.Title;
        person.Timestamp = personDTO.Timestamp;
        db.SaveChanges();
        return person;
    }

    [Fact]
    public async Task Fail_When_Timestamp_Differs()
    {
        using (var client = server.CreateClient().AcceptJson())
        {
            await client.PostAsJsonAsync(ApiEndpoint, Persons[0]);
            // 1
            var getResponse = await client.GetAsync(ApiEndpoint);
            var fetched = await getResponse.Content.ReadAsJsonAsync<List<Person>>();

            Assert.True(getResponse.IsSuccessStatusCode);
            Assert.NotEmpty(fetched);

            var person = fetched.First();
            // 2
            var fromDb = await db.Persons.SingleOrDefaultAsync(x => x.Id == person.Id);
            // 3
            fromDb.Title = "In between";
            // 4
            await db.SaveChangesAsync();


            // 5
            person.Title = "After - should fail";
            // 6
            var postResponse = await client.PutAsJsonAsync(ApiEndpoint + person.Id, person);
            var created = await postResponse.Content.ReadAsJsonAsync<Person>();

            Assert.False(postResponse.IsSuccessStatusCode);
        }
    }


    // generated sql - @p1 has the original timestamp from the entity and not the assigned and therefore the save succeed which was not intended
    exec sp_executesql N'SET NOCOUNT OFF;
    UPDATE[Person] SET[Title] = @p2
    OUTPUT INSERTED.[Timestamp]
    WHERE [Id] = @p0 AND[Timestamp] = @p1;
    ',N'@p0 int,@p1 varbinary(8),@p2 nvarchar(4000)',@p0=21,@p1=0x00000000000007F4,@p2=N'After - should fail'

Respuesta aceptada

Edit 4 - Fix

Me enteré de un miembro en el sitio de repo de GitHub, número 4512 . Tienes que actualizar el valor original en la entidad. Esto se puede hacer así.

var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
var entryProp = db.Entry(person).Property(u => u.Timestamp);
entryProp.OriginalValue = passedInTimestamp;

He actualizado la prueba de la unidad original que falló donde usted y yo no pudimos obtener la DbUpdateConcurrencyException , ahora funciona como se esperaba.

Actualizaré el ticket de GitHub para preguntar si pueden hacer un cambio para que el sql subyacente que se genera use el nuevo valor en lugar del valor original cuando la columna esté marcada como Timestamp o IsConcurrencyToken para que se comporte de manera similar a las versiones anteriores de Marco de la entidad.

Por ahora, aunque parece ser la forma de hacerlo con entidades separadas.


Editar # 3

Gracias, me lo perdí. Después de más depuración, una vez más, entiendo completamente el problema, aunque no por qué está ocurriendo. Sin embargo, probablemente deberíamos eliminar la API web, menos partes móviles y no creo que exista una dependencia directa entre EF Core y la API web. He reproducido el problema con las siguientes pruebas que ilustran el problema. No me atrevo a llamarlo un error, ya que tal vez la convención para obligar a EF Core a usar el valor pasado en la timestamp de timestamp haya cambiado desde EF6.

He creado un conjunto completo de código mínimo de trabajo y he creado un problema / pregunta en el sitio de GitHub del proyecto. Incluiré la prueba una vez más abajo para referencia. Tan pronto como me comunique, volveré a publicar esta respuesta y se lo haré saber.

Dependencias

  • Sql Server 2012
  • EF Core
    • EntityFramework.Commands 7.0.0-rc1-final
    • EntityFramework.MicrosoftSqlServer 7.0.0-rc1-final

DDL

var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
var entryProp = db.Entry(person).Property(u => u.Timestamp);
entryProp.OriginalValue = passedInTimestamp;

Entidad

var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
var entryProp = db.Entry(person).Property(u => u.Timestamp);
entryProp.OriginalValue = passedInTimestamp;

Contexto Db

var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
var entryProp = db.Entry(person).Property(u => u.Timestamp);
entryProp.OriginalValue = passedInTimestamp;

Prueba de unidad

var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
var entryProp = db.Entry(person).Property(u => u.Timestamp);
entryProp.OriginalValue = passedInTimestamp;

Respuesta popular

Microsoft ha actualizado su tutorial para esto en Manejo de conflictos de concurrencia - EF Core con ASP.NET Core MVC tutorial . Especifica específicamente lo siguiente con respecto a las actualizaciones:

Antes de llamar a SaveChanges , debe colocar el valor de la propiedad RowVersion original en la colección OriginalValues para la entidad.

_context.Entry(entityToUpdate).Property("RowVersion").OriginalValue = rowVersion;

Luego, cuando Entity Framework crea un comando SQL UPDATE, ese comando incluirá una cláusula WHERE que busca una fila que tenga el valor RowVersion original. Si no hay filas afectadas por el comando ACTUALIZAR (ninguna fila tiene el valor RowVersion original), Entity Framework lanza una excepción DbUpdateConcurrencyException .




Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué