ASP.NET Core 3.1 MVC with EF Core 3.1: how to get a record state before changing it? I am trying to capture a record (row) before I apply changes so I can create a record of the changes like 'is' and 'was'.
In my code while debugging I can see that both the Is and Was get changed with the Entity_Is.Status = "Closed".
How do I get the Was and Is?
The function AdminChanges writes the changes to a different table and does it with ExecuteSqlRaw so I don't need a model of it.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Close(int id)
{
TableName Entity_Is = _conn.TableName.Find(id);
TableName Entity_Was = _conn.TableName.Find(id);
_conn.Entry(Entity_Was).State = EntityState.Detached;
Entity_Is.Status = "Closed";
AdminChanges(kQC_Was, kQC_Is);
_conn.Update(Entity_Is);
await _conn.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
CodePudding user response:
Neither Entity Framework Core (nor EF6) have built-in support for creating (immutable) snapshot copies of loaded entities - you need to do that yourself.
In my own projects, I avoid EF and EFCore's anemic and spartan Scaffolding templates and instead use the infinitely better third-party EF POCO Generator tooling instead (disclaimer: I'm a project contributor) and I've modified the templates such that it generates code to allow me to create truly immutable snapshots of loaded entities.
I was going to suggest simply editing EF Core's scaffolding templates, but I dug-through Microsoft.EntityFrameworkCore.Design.dll and I saw that the CSharpEntityTypeGenerator class doesn't use any runtime templates (like T4, Liquid, Handlebars, etc) but it's all done internally - but it does look like you can still subclass CSharpEntityTypeGenerator and override the WriteCode method to call into the base implementation, then separately write out an immutable entity class. I don't know how you'd configure EF's tooling to use your subclass though.
...but something like this should work:
- You'll need to bring your own
static String StringJoin( this IEnumerable<String> values, String separator )extension method that forwards toString.Join. - I haven't tested this code at all, it's just to illustrate the principle of code-gen with EF Core's own code-gen tooling.
public class MutableAndImmutableCSharpEntityTypeGenerator : CSharpEntityTypeGenerator
{
public MutableAndImmutableCSharpEntityTypeGenerator( IAnnotationCodeGenerator annotationCodeGenerator, ICSharpHelper cSharpHelper )
: base( annotationCodeGenerator, cSharpHelper )
{
}
public override String WriteCode( IEntityType entityType, string? nomspace, bool useDataAnnotations, bool useNullableReferenceTypes )
{
String mutableClass = base.WriteCode( entityType, nomspace, useDataAnnotations, useNullableReferenceTypes );
String immutableClass = this.GenerateImmutableEntityClass( entityType );
// Then insert the `immutableClass` definition right before the last `}` in `mutableClass` (as that's the outer `namespace` block) and return it:
Int32 lastBraceIdx = mutableClass.LastIndexOf('}');
String bothClasses = mutableClass.Insert( startIndex: lastBraceIdx - 1, immutableClass );
return bothClasses;
}
private String GenerateImmutableEntityClass( IEntityType entityType )
{
const String IMMUTABLE_CLASS_TEMPLATE = @"
public class Immutable{0}
{{
public static Immutable{0} FromEntity( {0} entity )
{{
return new Immutable{0}(
{4}
);
}}
public Immutable{0}(
{1}
)
{{
{2}
}}
{3}
}}
";
IOrderedEnumerable<IProperty> valueProperties = entityType
.GetProperties()
.OrderBy( p => p.GetColumnOrder();
StringBuilder sb = new StringBuilder();
_ = sb.AppendFormat(
format: IMMUTABLE_CLASS_TEMPLATE,
/*{0}:*/ entityType.Name,
/*{1}:*/ valueProperties.Select( p => $"{p.ClrType.Name} {p.Name}" ).StringJoin( ",\r\n\t\t" ),
/*{2}:*/ valueProperties.Select( p => $"this.{p.Name} = {p.Name};" ;).StringJoin( "\r\n\t\t" ),
/*{3}:*/ valueProperties.Select( p => $"public {p.ClrType.Name} p.Name} {{ get; }}" ;).StringJoin( "\r\n\t\t" ),
/*{4}:*/ valueProperties.Select( p => $"{p.Name}: entity.{p.Name}" ;).StringJoin( "\r\n\t\t" ),
).AppendLine();
return sb.ToString();
}
}
Anyway, assuming you're able to get MutableAndImmutableCSharpEntityTypeGenerator running at all, then it should give you output like this (assuming the entity is a cliche Customer type):
// This is the entity type that EF generates for you already:
public class Customer
{
public String FirstName { get; set; }
public String LastName { get; set; }
public String StreetAddress { get; set; }
}
// This is the immutable entity snapshot type that `MutableAndImmutableCSharpEntityTypeGenerator` generates:
public class ImmutableCustomer
{
public static ImmutableCustomer FromEntity( Customer entity )
{
return new ImmutableCustomer(
FirstName: entity.FirstName,
LastName: entity.LastName,
StreetAddress : entity.StreetAddress
);
}
public ImmutableCustomer(
String FirstName,
String LastName,
String StreetAddress
)
{
this.FirstName = FirstName;
this.LastName= LastName;
this.StreetAddress = StreetAddress;
}
public String FirstName { get; }
public String LastName { get; }
public String StreetAddress { get; }
}
...which will let you do this:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Close(int customerId)
{
Customer? customer = _conn.Customers.Find( customerId );
if( customer is null ) return this.NotFound();
ImmutableCustomer immCus = ImmutableCustomer.FromEntity( customer );
customer.FirstName = "Closed";
AdminChanges( customer , immCus ); // <-- I suppose this is an audit-logging function?
_conn.Update( customer );
_ = await _conn.SaveChangesAsync();
return RedirectToAction(nameof(this.Index));
}
Some exercises for the reader:
- If you're running on .NET 6 then consider changing
public class Immutable{0}to be a record-type instead:public record class Immutable{0}. - The code-generator above will generate the
public static Immutable{0} FromEntity( {0} entity )factory method, but not the reverse operation: populating a mutable entity object from anImmutable{0}object instance. That should be trivial to implement. - The generated code uses
PascalCasefor parameter names, which is bad form, update the code to render parameters (and locals) incamelCasebut retain public properties inPascalCase. - Also consider adding
#nullableannotations in a way that makes sense given your project requirements. - The generated code only snapshots scalar-valued properties on the entity object, it does not snapshot any navigation-properties or collection-properties - snapshotting those is non-trivial and you need to really think about how that should work given that EF does not provide any refinement-types to represent fully-loaded entity objects (again, my own code-gen does this for me, I'm surprised EF Core doesn't include this super-essential functionality, otherwise working with partially-loaded entity objects is a minefield).
CodePudding user response:
You want to use the change tracker (DbContext.ChangeTracker). It allows you to review each property for changes and grants you access to the original and current values. Read this page for an overview and examples that should cover your use case(s).
