Handling soft delete

In this recipe, we will cover how to implement a soft delete solution with the Entity Framework, leveraging an open source library of interceptors.

Getting ready

We will be using the NuGet Package Manager to install the Entity Framework 4.2 assemblies, and the Entity Framework Interceptors assemblies.

The package installer can be found at http://nuget.codeplex.com/.

We will also be using a database for connecting to the data and updating it.

We then the following command:

Install-Package Isg.EntityFramework.Interceptors.SoftDelete

Open the Improving Soft Delete solution in the included source code examples.

How to do it...

  1. We start by writing out a test that will allow us to define the scope of our problem. In this case, we want to deal with soft deletable objects in the same way as we deal with other objects in our context. We define it with the following code:
    using System;
    using System.Data.Entity;
    using System.Linq;
    using BusinessLogic;
    using DataAccess;
    using Isg.EntityFramework.Interceptors;
    using Isg.EntityFramework.Interceptors.SoftDelete;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Test
    {
      [TestClass]
      public class QueryTests
      {
        [TestMethod]
        public void DeleteShouldSetIsDeleted()
        {
          var title = Guid.NewGuid().ToString();
          InterceptorProvider.SetInterceptorProvider(
            new DefaultInterceptorProvider(
            new SoftDeleteChangeInterceptor()));
          Database.SetInitializer(new DropCreateDatabaseAlways<BlogContext>());
    
          using (var db = new BlogRepository(new BlogContext()))
          {
            var customer = new Blog {IsDeleted = false, Title = title};
            db.Add(customer);
            db.SaveChanges();
          }
    
          using (var db = new BlogRepository(new BlogContext()))
          {
            var customer = db.Set<Blog>().SingleOrDefault(i => i.Title == title);
            db.Remove(customer);
            db.SaveChanges();
          }
    
          using (var db = new BlogRepository(new BlogContext()))
          {
            var customer = db.Set<Blog>().SingleOrDefault(i => i.Title == title);
            Assert.IsNotNull(customer);
            Assert.AreEqual(true, customer.IsDeleted);
          }
        }
    
      }
    
    }
  2. We then add our Blog object as a new C# file named Blog.cs to our BusinessLogic project, so that we have an example object to connect to, with the following code:
    using System;
    using Isg.EntityFramework.Interceptors.SoftDelete;
    
    namespace BusinessLogic
    {
      public class Blog : ISoftDelete
      {
        public Blog()
        {
          CreationDate = DateTime.UtcNow;
          ArchiveDate = DateTime.UtcNow;
        }
        public int Id { get; set; }
        public DateTime CreationDate { get; set; }
        public string ShortDescription { get; set; }
        public string Title { get; set; }
        public double Rating { get; set; }
        public DateTime ArchiveDate { get; set; }
        public bool IsDeleted { get; set; }
      }
    }
  3. We then add our Mapping as a new C# file named BlogMapping to the DataAccess project, with the following code:
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.ModelConfiguration;
    using BusinessLogic;
    
    namespace DataAccess.Mappings
    {
      public class BlogMapping : EntityTypeConfiguration<Blog>
      {
        public BlogMapping()
        {
          this.ToTable("Blogs");
          this.HasKey(x => x.Id);
    
          this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("BlogId");
          this.Property(x => x.Title).IsRequired().HasMaxLength(250);
    
          this.Property(x => x.CreationDate).HasColumnName("CreationDate").IsRequired();
    
          this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description");
    
          this.Property(x => x.IsDeleted).HasColumnName("DeletedFlag");
        }
    
      }
    }
  4. The next step is to add the mapping to the context and make the context inherit from our Interceptors base class with the following code:
    using System.Data.Entity;
    using System.Linq;
    using DataAccess.Mappings;
    using Isg.EntityFramework.Interceptors;
    
    namespace DataAccess
    {
      public class BlogContext : InterceptorDbContext, IUnitOfWork
      {
        public void Add<T>(T entity) where T : class
        {
          this.Set<T>().Add(entity);
        }
    
        public void Commit()
        {
          SaveChanges();
        }
    
        public IQueryable<T> Find<T>() where T : class
        {
          return this.Set<T>();
        }
    
        public void Refresh()
        {
          this.ChangeTracker.Entries().ToList().ForEach(x => x.Reload());
        }
    
        public void Remove<T>(T entity) where T : class
        {
          this.Set<T>().Remove(entity);
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
          modelBuilder.Configurations.Add(new BlogMapping());
          base.OnModelCreating(modelBuilder);
        }
      }
    }
  5. Then, we modify the IUnitOFWork interface with the following code:
    using System.Linq;
    
    namespace DataAccess
    {
      public interface IUnitOfWork
      {
        IQueryable<T> Find<T>() where T : class;
        void Refresh();
        void Commit();
        void Remove<T>(T entity) where T : class;
        void Add<T>(T entity) where T : class;
      }
    }
  6. Then, we modify the IBlogRepository interface with the following code:
    using System.Linq;
    
    namespace BusinessLogic
    {
      public interface IRepository
      {
        void RollbackChanges();
        void SaveChanges();
        IQueryable<T> Set<T>() where T : class;
        void Add<T>(T entity) where T : class;
        void Remove<T>(T entity) where T : class;
      }
    }
  7. We then modify BlogRepository to implement the interface with the following code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using BusinessLogic;
    using BusinessLogic.Queries;
    using DataAccess.Queries;
    
    namespace DataAccess
    {
      public class BlogRepository : IRepository, IDisposable
      {
        private readonly IUnitOfWork _context;
    
        public BlogRepository(IUnitOfWork context)
        {
          _context = context;
        }
    
        public IQueryable<T> Set<T>() where T : class
        {
          return _context.Find<T>();
        }
    
        public void RollbackChanges()
        {
          _context.Refresh();
        }
    
        public void SaveChanges()
        {
          try
          {
            _context.Commit();
          }
          catch (Exception)
          {
            RollbackChanges();
            throw;
          }
    
        }
        public void Dispose()
        {
    
        }
    
        public void Add<T>(T entity) where T : class
        {
          _context.Add(entity);
        }
    
        public void Remove<T>(T entity) where T : class
        {
          _context.Remove(entity);
        }
      }
    }
  8. All of our tests pass.

How it works...

Our test defines that we want to be able to add an object, delete it, and the object still exists, but in a deleted state (soft deleted). This defines the scope of the problem, and makes sure that we don't over engineer the solution. This is one of those problems where it is easy to provide too many solutions. We don't want some giant profiling solution that modifies everything to and from the database, or anything that extreme, and our test will help us limit ourselves to solving the problem that we have defined.

We define our Blog object which we will use to exercise the interceptors. Notice that we implement the ISoftDelete interface. This allows the interceptor to catch the saves of deletes for this object, and mark the flag instead of committing a true delete.

We then define our context, and inherit from the interceptor context. This gives us the hooks to intercept the save changes call and modify the behavior as we need. In this case, the soft delete.

The repository is implemented to give us a clean interface to interact with and hide some of the implementation details from our consuming code.

There's more...

There are a couple of details that will help us understand the problem in more depth, and enhance our solution.

Entity framework interceptors

This is an open source project by Chris McKenzie that gives a standard implementation to the common extension points for the Entity Framework. It is well written and will save time, if you are considering rolling your own interception.

http://nuget.org/packages/Isg.EntityFramework.Interceptors

Soft delete pattern

The soft delete pattern (or anti-pattern, as the case may be argued) is the pattern by which the data is put into an inactive state, and is no longer displayed or interacted with by the system. This normally comes in the form of additional properties on the object that mark it as deleted with a date time, or active date ranges. This pattern ensures that no data is ever truly lost. There are many blogs and discussions on this pattern being good or bad. They are all based on opinion at this point, but this one is for certain. This is in production in a lot of places, and you have to deal with it for the foreseeable future.

See also

In this chapter:

  • Capturing the Audit Data
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset