Estoy tratando de configurar la función de guardado automático de auditoría de EntityFramework Plus , pero parece que estoy atascado en algo muy tonto. Estoy siguiendo la ruta "Guardar automáticamente anulando SaveChanges & SaveChangesAsync", pero estoy tratando de usar el código primero ya que el proyecto para el que voy a usar eso se ha estado ejecutando así por un tiempo. Dicho esto, mi DbContext se ve así:
public class CadastralDbContext : DbContext
{
public CadastralDbContext(DbContextOptions<CadastralDbContext> options) : base(options) { }
static CadastralDbContext()
{
AuditManager.DefaultConfiguration.AutoSavePreAction = (context, audit) =>
(context as CadastralDbContext).AuditEntries.AddRange(audit.Entries);
}
public DbSet<AuditEntry> AuditEntries { get; set; }
public DbSet<AuditEntryProperty> AuditEntryProperties { get; set; }
//Ommited my DbSets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(CadastralDbContext).Assembly);
/*** Ignore these for now ***/
//modelBuilder.Entity<AuditEntry>().Ignore(x => x.Properties);
//modelBuilder.Entity<AuditEntryProperty>().Ignore(x => x.Parent);
}
public override int SaveChanges()
{
var audit = new Audit();
audit.PreSaveChanges(this);
var rowAffecteds = base.SaveChanges();
audit.PostSaveChanges();
if (audit.Configuration.AutoSavePreAction != null)
{
audit.Configuration.AutoSavePreAction(this, audit);
base.SaveChanges();
}
return rowAffecteds;
}
public async Task<int> SaveChangesAsync()
{
return await SaveChangesAsync(CancellationToken.None);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken)
{
var audit = new Audit();
audit.PreSaveChanges(this);
var rowAffecteds = await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
audit.PostSaveChanges();
if (audit.Configuration.AutoSavePreAction != null)
{
audit.Configuration.AutoSavePreAction(this, audit);
await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
return rowAffecteds;
}
}
}
Básicamente, lo que dice el tutorial con DbSet<AuditEntry>
y DbSet<AuditEntryProperty>
que son clases del propio marco. Al inspeccionar los metadatos para estos, tenemos:
//
// Summary:
// An audit entry.
public class AuditEntry
{
//
// Summary:
// Gets or sets the object state entry.
[NotMapped]
public object Entity;
//
// Summary:
// Gets or sets the object state entry.
[NotMapped]
public EntityEntry Entry;
//
// Summary:
// Gets or sets the parent.
public Audit Parent;
public AuditEntry();
//
// Summary:
// Gets or sets the identifier of the audit entry.
[Column(Order = 0)]
public int AuditEntryID { get; set; }
//
// Summary:
// Gets or sets who created this object.
[Column(Order = 5)]
[MaxLength(255)]
public string CreatedBy { get; set; }
//
// Summary:
// Gets or sets the the date of the changes.
[Column(Order = 6)]
public DateTime CreatedDate { get; set; }
//
// Summary:
// Gets or sets the name of the entity set.
[Column(Order = 1)]
[MaxLength(255)]
public string EntitySetName { get; set; }
//
// Summary:
// Gets or sets the name of the entity type.
[Column(Order = 2)]
[MaxLength(255)]
public string EntityTypeName { get; set; }
//
// Summary:
// Gets or sets the properties.
public List<AuditEntryProperty> Properties { get; set; }
//
// Summary:
// Gets or sets the entry state.
[Column(Order = 3)]
public AuditEntryState State { get; set; }
//
// Summary:
// Gets or sets the name of the entry state.
[Column(Order = 4)]
[MaxLength(255)]
public string StateName { get; set; }
}
Y
//
// Summary:
// An audit entry property.
public class AuditEntryProperty
{
//
// Summary:
// Gets or sets the new value audited.
[NotMapped]
public PropertyEntry PropertyEntry;
public object NewValue;
public object OldValue;
public AuditEntryProperty();
//
// Summary:
// Gets or sets the name of the property internally.
[NotMapped]
public string InternalPropertyName { get; set; }
//
// Summary:
// Gets or sets a value indicating whether OldValue and NewValue is set.
[NotMapped]
public bool IsValueSet { get; set; }
//
// Summary:
// Gets or sets the name of the relation audited.
[Column(Order = 2)]
[MaxLength(255)]
public string RelationName { get; set; }
//
// Summary:
// Gets or sets the name of the property audited.
[Column(Order = 3)]
[MaxLength(255)]
public string PropertyName { get; set; }
//
// Summary:
// Gets or sets the parent.
public AuditEntry Parent { get; set; }
//
// Summary:
// Gets or sets the identifier of the audit entry property.
[Column(Order = 0)]
public int AuditEntryPropertyID { get; set; }
//
// Summary:
// Gets or sets the new value audited formatted.
[Column("NewValue", Order = 5)]
public string NewValueFormatted { get; set; }
//
// Summary:
// Gets or sets the identifier of the audit entry.
[Column(Order = 1)]
public int AuditEntryID { get; set; }
//
// Summary:
// Gets or sets the old value audited formatted.
[Column("OldValue", Order = 4)]
public string OldValueFormatted { get; set; }
}
Se ve lo suficientemente bueno como para guardar dos propiedades: public List<AuditEntryProperty> Properties { get; set; }
y public AuditEntry Parent { get; set; }
. Como no están marcados como virtual
, fallará la adición de una migración. Intenté una solución solo para ver si podía lograr que generara las tablas y realmente tuve éxito (esas líneas comentaron anteriormente):
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//...
modelBuilder.Entity<AuditEntry>().Ignore(x => x.Properties);
modelBuilder.Entity<AuditEntryProperty>().Ignore(x => x.Parent);
}
Eso parece deshabilitar la relación PrimaryKey-ForeignKey que tienen ambas tablas, que se configuran dentro del propio marco, ya que no hay indicios de que deba hacerlo manualmente. Incluso intenté ejecutar el script solo para ver qué saldría de él, y los resultados fueron catastróficos:
CREATE INDEX [IX_AuditEntryID] ON [dbo].[AuditEntryProperties]([AuditEntryID])
GO
ALTER TABLE [dbo].[AuditEntryProperties]
ADD CONSTRAINT [FK_dbo.AuditEntryProperties_dbo.AuditEntries_AuditEntryID]
FOREIGN KEY ([AuditEntryID])
REFERENCES [dbo].[AuditEntries] ([AuditEntryID])
ON DELETE CASCADE
GO
Eso me provocó el siguiente error de SQL al insertarlo: la String or binary data would be truncated
. Así que acabo de regresar al estado anterior, donde el marco tiene un "50% de salida", ya que guarda registros en la tabla AuditEntry (que contiene datos como la tabla) cada vez que un usuario solicita una operación de inserción, actualización o eliminación, pero nada persistirá en AuditEntryProperties (nuevo valor, antiguo valor, columna) y no puedo pensar en otra cosa que no se ignore esas propiedades como la causa de todo esto.
Pensé que podría anular AuditEntry y AuditEntryProperties, pero eso suena como una gran solución estúpida. No soy un experto en DB, ¿qué me estoy perdiendo aquí?
Editar : Olvidé agregar el código de migración:
migrationBuilder.CreateTable(
name: "AuditEntries",
columns: table => new
{
AuditEntryID = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
CreatedBy = table.Column<string>(maxLength: 255, nullable: true),
CreatedDate = table.Column<DateTime>(nullable: false),
EntitySetName = table.Column<string>(maxLength: 255, nullable: true),
EntityTypeName = table.Column<string>(maxLength: 255, nullable: true),
State = table.Column<int>(nullable: false),
StateName = table.Column<string>(maxLength: 255, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditEntries", x => x.AuditEntryID);
});
migrationBuilder.CreateTable(
name: "AuditEntryProperties",
columns: table => new
{
AuditEntryPropertyID = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
AuditEntryID = table.Column<int>(nullable: false),
PropertyName = table.Column<string>(maxLength: 255, nullable: true),
RelationName = table.Column<string>(maxLength: 255, nullable: true),
NewValue = table.Column<string>(nullable: true),
OldValue = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditEntryProperties", x => x.AuditEntryPropertyID);
});
Edit 2 Intentó agregar el FK con Fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(CadastralDbContext).Assembly);
modelBuilder.Entity<AuditEntryProperty>().HasOne<AuditEntry>(prop => prop.Parent).WithMany(a => a.Properties).HasForeignKey(prop => prop.AuditEntryID);
}
La migración aún no se puede realizar ya que esas propiedades no son virtuales.
Creamos un problema en el Rastreador de problemas de EF Plus
Encontrará aquí un proyecto que puede probar, le sugiero que continúe la discusión sobre nuestro Rastreador de problemas, ya que Stack Overflow no es una plataforma para este tipo de problemas.