Last active
October 19, 2018 14:56
-
-
Save phillippelevidad/2ffb6454f69bc5a89aeca998876679aa to your computer and use it in GitHub Desktop.
EF Core 2 implementation of SoftDeletes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* EF Core implementation of SoftDeletes. | |
* | |
* Just specify entity types that should not be hard deleted in the SoftDeleteMapper class | |
* and this code will handle the rest, by: | |
* 1. Adding the IsDeleted and DeletedAt columns; | |
* 2. Applying a query filter to prevent soft-deleted entries from being returned on reads; | |
* 3. Automatically rewriting deletes so that they only set entries as deleted, preventing hard-deletes. | |
*/ | |
namespace Infra.Data.Mapping | |
{ | |
interface IMapper | |
{ | |
void Map(ModelBuilder modelBuilder); | |
} | |
class SoftDeleteMapper : IMapper | |
{ | |
public static Type[] Types = new Type[] | |
{ | |
// TODO: specify entity types. | |
typeof(Foo), | |
typeof(Bar), | |
typeof(FooBar) | |
}; | |
public void Map(ModelBuilder modelBuilder) | |
{ | |
foreach (var type in Types) | |
{ | |
var entityType = modelBuilder.Entity(type); | |
// Add properties. | |
entityType.Property<bool>("IsDeleted"); | |
entityType.Property<DateTime?>("DeletedAt"); | |
// Auto filter queries by IsDeleted. | |
var parameter = Expression.Parameter(type); | |
// EF.Property<bool>(entity, "IsDeleted") | |
var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool)); | |
var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted")); | |
// EF.Property<bool>(post, "IsDeleted") == false | |
var compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false)); | |
// entity => EF.Property<bool>(entity, "IsDeleted") == false | |
var lambda = Expression.Lambda(compareExpression, parameter); | |
entityType.HasQueryFilter(lambda); | |
} | |
} | |
} | |
} | |
namespace Infra.Data | |
{ | |
public class AppDbContext : DbContext | |
{ | |
protected override void OnModelCreating(ModelBuilder modelBuilder) | |
{ | |
base.OnModelCreating(modelBuilder); | |
var mappers = new IMapper[] | |
{ | |
new SoftDeleteMapper() | |
}; | |
foreach (var m in mappers) | |
m.Map(modelBuilder); | |
} | |
public override int SaveChanges() | |
{ | |
OnBeforeSaving(); | |
base.SaveChanges(); | |
} | |
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) | |
{ | |
OnBeforeSaving(); | |
await base.SaveChangesAsync(cancellationToken); | |
} | |
void OnBeforeSaving() | |
{ | |
RewriteDeletesForSoftDeletableEntities(); | |
} | |
void RewriteDeletesForSoftDeletableEntities() | |
{ | |
foreach (var type in SoftDeleteMapper.Types) | |
{ | |
foreach (var entry in ChangeTracker.Entries()) | |
{ | |
if (!type.IsAssignableFrom(entry.Entity.GetType())) | |
continue; | |
if (entry.State == EntityState.Deleted) | |
{ | |
entry.State = EntityState.Modified; | |
entry.CurrentValues["IsDeleted"] = true; | |
entry.CurrentValues["DeletedAt"] = DateTime.UtcNow; | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment