简体   繁体   中英

Passing generic func as Linq expression C#

I have a Linq expression that is repeating over multiple places. I want to centrally define the Linq expression and use it in all such places. Here is the code:

 public interface ISoftDelete
 {
    DateTime? DeletedOn { get; set; }
 }

 public class BaseModel : ISoftDelete
 {
    public int Id { get; set; }
    public DateTime? DeletedOn { get; set; }
 }

 public class Epic: BaseModel {
 }

 public class Feature: BaseModel {
 }

 public class UserStory: BaseModel {
 }

 public class ProductFocusDbContext : DbContext
 {
        public ProductFocusDbContext(DbContextOptions<ProductFocusDbContext> options) : base(options) { }

        public DbSet<Epic> Epics { get; set; }
        public DbSet<Feature> Features { get; set; }
        public DbSet<UserStory> UserStories { get; set; }

      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {

            // Epic
            modelBuilder.Entity<Epic>().HasQueryFilter(ExcludeSoftDeleted);

            // Feature
            modelBuilder.Entity<Feature>().HasQueryFilter(x => x.DeletedOn == null);

            // User Story
            modelBuilder.Entity<UserStory>().HasQueryFilter(x => x.DeletedOn == null);

      }

       System.Linq.Expressions.Expression<Func<ISoftDelete, bool>> ExcludeSoftDeleted = (x => x.DeletedOn == null) ;
}

I want to replace all occurrences of x => x.DeletedOn == null with ExcludeSoftDeleted, but I get following exception when I try do it using the above code:

InvalidOperationException: The filter expression 'x => (x.DeletedOn == null)' specified for entity type 'Epic' is invalid. The expression must accept a single parameter of type 'ProductFocus.Domain.Model.Epic', return bool, and may not contain references to navigation properties.

How can I achieve it?

HasQueryFilter is a generic method where the generic parameter T matches that of the previous call to Entity<EntityType> . You have no problem passing an expression manually as the appropriate types get used. However, the expression property you tried passing is of type Expression<Func<ISoftDelete, bool>> and there is no implicit conversion from Expression<Func<EntityType, bool>> , even if EntityType implements ISoftDelete ( Expression<> is not covariant), which is why it does not work.

You can get around this by providing some helper classes that can return you the appropriate expression for your entity.

public static class SoftDeleteHelper<T> 
    where T: ISoftDelete // constrain generic type to interface 
{
    public static Expression<Func<T, bool>> ExcludeSoftDeleted 
        => (x => x.DeletedOn == null):
}

And then you can refer to this inside of your query filter:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Epic>().HasQueryFilter(SoftDeleteHelper<Epic>.ExcludeSoftDeleted);
    modelBuilder.Entity<Feature>().HasQueryFilter(SoftDeleteHelper<Feature>.ExcludeSoftDeleted);
    modelBuilder.Entity<UserStory>().HasQueryFilter(SoftDeleteHelper<UserStory>.ExcludeSoftDeleted);
} 

The key here is that we've constrained the generic parameter to ISoftDelete which guarantees that the DeletedOn property exists on the entity type.

Alternatively you could define this as a method that returns your entity expression. This may be more suitable if you have other query filters that need to be constrained to different interfaces:

public static class ExpressionHelper
{
    public static Expression<Func<T, bool>> ExcludeSoftDeleted<T>() 
        where T: ISoftDelete // constrained to interface
        => (x => x.DeletedOn == null);
}

Which can then be used like the following (note this is different from above and requires () as you are invoking a function that returns the expression and not referencing a property)

modelBuilder.Entity<Epic>().HasQueryFilter(ExpressionHelper.ExcludeSoftDeleted<Epic>())

You could go a step further and write extension methods on ModelBuilder or EntityTypeBuilder<T> constrained to the generic type and omit the helper class altogether

public static class EntityBuilderExtensions
{
    // extension method on the main builder 
    public static EntityTypeBuilder<T> EntityWithSoftDelete<T>(
        this ModelBuilder builder) 
        where T: class, ISoftDelete // extra class constraint required by Entity<>
    {
        return builder.Entity<T>().WithSoftDelete();
    }
    // extension method on the result of Entity<T>
    public static EntityTypeBuilder<T> WithSoftDelete<T>(
        this EntityTypeBuilder<T> builder) 
        where T: class, ISoftDelete // extra class constraint required by Entity<>
    {
        return builder.HasQueryFilter(
            e => e.DeletedOn == null 
        );
    } 
} 

This works once again thanks to the generic constraint on ISoftDelete . You can then call them like:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Epic>().WithSoftDelete();
    // or
    modelBuilder.EntityWithSoftDelete<Feature>();
} 

The methods return an EntityTypeBuilder<T> which you can then use to chain further entity configurations.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM