3
\$\begingroup\$

This system allows the user to define property mappings between two objects and a filter. Then the user may call Filters.Apply<T>() to get an IQueryable<T>.

Example Setup

 // Called from some program entry point.
 Filters.Configure()
        .Map<TileVM, Tile, Contains>(x => x.SearchName, x => x.Name)
        .Map<TileVM, Tile, Multiple>(x => x.SearchType, x => x.Type)
        .Map<TileVM, Tile, LessThan>(x => x.SearchX, x => x.X)
        .Map<TileVM, Tile, GreaterThanOrEqual>(x => x.SearchY, x => x.Y)
        .Map<TileVM, Tile, NotEqual>(x => x.SearchNotBroken, x => x.IsBroken);

    public class TileVM
    {
        public string SearchName { get; set; }
        public string SearchType { get; set; }
        public int? SearchX { get; set; }
        public int? SearchY { get; set; }
        public bool? SearchNotBroken { get; set; }
    }

    public class Tile
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public bool IsBroken { get; set; }
    }

Example Usage

var vm = TileVM() { ... };
var Db = new DbContext();
var query = Filters.Apply(vm, Db.Tiles.ReadAll());

Code

/// <summary>
/// Defines an interface for producing a filter expression (predicate) for a type given a property selector on that type and a criteria/search value.
/// </summary>
public interface IFilter
{
    Expression<Func<T, bool>> GetFilterExpression<T>(object searchValue, Expression<Func<T, object>> filterPropertySelector);
}

/// <summary>
/// Allows the configuration and application of filters from one object type's property to another object type's property.
/// Recommended placing configuration in a App_Start and then calling it in the Application_Start() in Global.asax.
/// </summary>
public class Filters
{
    /// <summary>
    /// All mappings currently configured for the application.
    /// The key being the model type and the value being a collection of mappings from one property to another.
    /// </summary>
    protected static IDictionary<Type, IList<FilterMapping>> Mappings = new Dictionary<Type, IList<FilterMapping>>();

    /// <summary>
    /// Allows the configuration of mappings from a search model's property value to a model's property value,
    /// with value matching determined by the indicated IFilter type.
    /// </summary>
    public Filters Map<TVM, T, TF>(Expression<Func<TVM, object>> searchProperty, Expression<Func<T, object>> filterProperty)
    {
        var modelType = typeof(T);
        if (!Mappings.ContainsKey(modelType))
        {
            Mappings.Add(modelType, new List<FilterMapping>());
        }

        Mappings[modelType].Add(new FilterMapping()
        {
            SearchType = typeof(TVM),
            ModelType = typeof(T),
            FilterType = typeof(TF),
            SearchPropertySelector = searchProperty,
            FilterPropertySelector = filterProperty
        });
        return this;
    }

    /// <summary>
    /// Applies the filters to the given source query using the values from the search object.
    /// </summary>
    public static IQueryable<T> Apply<TVM, T>(TVM searchObject, IQueryable<T> source)
    {
        var modelType = typeof(T);
        if (!Mappings.ContainsKey(modelType))
        {
            throw new ApplicationException(String.Format("Filters have not yet been configured for a model of {0}.", modelType.FullName));
        }

        var mappings = Mappings[modelType];
        foreach (var mapping in mappings)
        {
            var searchValue = (mapping.SearchPropertySelector as Expression<Func<TVM, object>>).Compile().Invoke(searchObject);
            var filterProvider = Activator.CreateInstance(mapping.FilterType) as IFilter;
            var predicate = filterProvider.GetFilterExpression<T>(searchValue, mapping.FilterPropertySelector as Expression<Func<T, object>>);

            source = source.Where(predicate);
        }

        return source;
    }

    /// <summary>
    /// Starts a fluent chain of mapping filters to object types.
    /// </summary>
    public static Filters Configure()
    {
        return new Filters();
    }

    /// <summary>
    /// Class to contain all filter mapping information.
    /// </summary>
    protected class FilterMapping
    {
        public Type SearchType { get; set; }
        public Type ModelType { get; set; }
        public Type FilterType { get; set; }

        public Expression SearchPropertySelector { get; set; }
        public Expression FilterPropertySelector { get; set; }
    }
}
\$\endgroup\$
4
\$\begingroup\$
  1. You shouldn't have to compile this on every search:

    (mapping.SearchPropertySelector as Expression<Func<TVM, object>>).Compile()
    

    Instead you should be able to store the compiled delegate when setting up the mapping.

  2. Similarly it might be an option to pre-create the IFilter instance - either per FilterType (becomes a singleton) or per mapping. Given that these filter instances seem like they should be stateless anyway having them as per-type singleton might be a viable approach.

  3. In Map<TVM, T, TF> TF should be restricted with where TF: IFilter or else your Apply may throw a NullReferenceException

  4. Since in Apply you currently can't deal with filterProvider being null you should not use as but a direct cast instead.

\$\endgroup\$
3
\$\begingroup\$

Whenever you also need the value for a given key of a Dictionary<TKey, TValue> you shouldn't use ContainsKey() together with the Item getter but you should use TryGetValue().

Internally these three methods are calling the FindEntry() method to check wether a given key exists. So calling this method only once through the TryGetValue() method should be the way to go.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the suggestion. Is this soley for performance reasons or are there other reasons I'm not picking up on? (Just out of curiousity). \$\endgroup\$ – Shelby115 Nov 17 '15 at 22:35
  • \$\begingroup\$ Performance and best practice. \$\endgroup\$ – Heslacher Nov 18 '15 at 6:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.