Search code examples
c#.netentity-frameworksoft-deleteglobal-query-filter

Requests adaptable to include or omit Soft Deleted data


I implemented the Soft Deletes guide from JetBrains.
Summary:

  • add DeletionStatus as a property on the database model YourObject
  • add global query filter in EntityFramework YourObjectConfiguration to always exclude deleted objects
    builder.HasQueryFilter(el => el.DeletionStatus != DeletionStatus.Deleted)
  • in requests where you need soft deleted data, use: context.YourObjects.IgnoreQueryFilter().Include(..)

I use services and repositories, where the repositories make the db context calls.
The problem is, I end up having duplicate methods for any Get/Fetch. For some requests I need to ignore deleted data for others, I need all table records.

MyObjectRepository.cs

public Task<MyObject> GetByIdWithFilesAsync(int id)
{
    return context.MyObjects
        .Include(el => el.FileMyObjects)
        .SingleOrDefaultAsync(el => el.Id == id);
}

public Task<MyObject> GetByIdWithFilesWithIgnoreFiltersAsync(int id)
{
    return context.MyObjects
        .IgnoreQueryFilters()
        .Include(el => el.FileMyObjects)
        .SingleOrDefaultAsync(el => el.Id == id);
}

Throughout the application there are many places where I need to duplicate repository methods and also add a long name suffix 'WithIgnoreFilters'. I feel like I am doing something wrong and there must be some good practice.

Do you know a better a way to implement this? These are some options but I am not sure they are better

  • optional parameter (i.e. adding if check in every method)
  • custom attribute

Solution

  • I would suggest going with optional parameter. But this would result in many ifs being written in each of the method, which would not scale well.

    But I would suggest creating extension method:

    public static IQueryable<TEntity> OptionallyIgnoreQueryFilters<TEntity>(
        this IQueryable<TEntity> query,
        bool? ignoreFilters)
        where TEntity : class
    {
        return ignoreFilters.HasValue && ignoreFilters.Value
            ? query.IgnoreQueryFilters()
            : query;
    }
    

    This would minimize the impact of changes, as they would require just adding optional parameter ignoreFilters (it's up to you if null will be allowed and how to handle it), and adding every whereOptionallyIgnoreQueryFilters(ignoreFilters), instead of if statement.