65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ASP.NET 动态查询 Entity Framework

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2017年5月15日

CPOL

3分钟阅读

viewsIcon

28728

downloadIcon

625

一个用于在 ASP.NET 中动态查询 Entity Framework DbContext 的即用型解决方案

引言

本文描述了一种简单、易于理解且动态的方式来处理 ASP.NET 项目中的 Entity Framework 查询。 主要目标是提供一种动态且可重用的方式来对 Entity Framework DbContext 执行复杂搜索,以及分页和过滤操作。

目录

  1. QueryFilter:在本节中,我们将创建所有过滤器的基类
  2. QueryFilter 示例:在本节中,我们将创建第一个 QueryFilter
  3. QueryViewModel:在本节中,我们将创建帮助我们与视图来回通信的类
  4. ModelAbstractionBinder:在本节中,我们将创建自己的 IModelBinder,以便将我们的 abstract QueryFilter 类绑定到它的实现
  5. 使用代码:学习如何使用代码

1. QueryFilter

QueryFilter 是所有我们将创建的过滤器将继承的 abstract 类。
它公开一个属性 Discriminator,该属性将用于存储其实现类型的名称,并且它定义一个 abstract Filter() 方法 - 以及它的泛型重载 - 在这里奇迹将发生。

    /// <summary>
    /// Defines the basic interaction logic of all 
    /// <see cref="QueryFilter"/> implementations
    /// </summary>
    [ModelBinder(typeof(ModelAbstractionBinder<QueryFilter>))]
    public abstract class QueryFilter
    {

        /// <summary>
        /// Gets/sets a string that represents the 
        /// <see cref="QueryFilter"/>'s discriminator, 
        /// which is the type name of the <see cref="QueryFilter"/> implementation
        /// </summary>
        public string Discriminator { get; set; }

        /// <summary>
        /// Filters the specified query
        /// </summary>
        /// <param name="entityType">The type of the query</param>
        /// <param name="query">The query to filter</param>
        /// <returns>The filtered query</returns>
        public abstract IQueryable Filter(Type entityType, IQueryable query);

        /// <summary>
        /// Filters the specified query
        /// </summary>
        /// <typeparam name="TEntity">The type of the query to filter</typeparam>
        /// <param name="query">The query to filter</param>
        /// <returns>The filtered query</returns>
        public IQueryable<T> Filter<T>(IQueryable<T> query)
        {
            object result;
            result = this.Filter(typeof(T), query);
            return result as IQueryable<T>;
        }
    }

请看修饰我们类的属性?它告诉 MVC 我们将依靠我们自己的 IModelBinder 来完成绑定工作。 为什么呢? 简单来说,因为 MVC 默认使用的 DefaultModelBinder 显然无法绑定到 abstract 类。 现在,还记得我们在 QueryFilter 类中的 Discriminator 属性吗?这就是我们的 ModelAbstractionBinder 将用来帮助 DefaultModelBinder 绑定到正确的类。

2. QueryFilter 示例

比起用很多文字说明整个过程是如何运作的,让我们创建自己的 OrderByFilter,我们将使用它来对查询进行排序。

/// <summary>
    /// Represents a <see cref="QueryFilter"/> 
    /// used to order the results of a query
    /// </summary>
    public class OrderByFilter
        : QueryFilter
    {

        private static readonly MethodInfo OrderByMethod = 
           typeof(Queryable).GetMethods().Single
           (m => m.Name == "OrderBy" && m.GetParameters().Length == 2);
        private static readonly MethodInfo OrderByDescendingMethod = 
        typeof(Queryable).GetMethods().Single(m => m.Name == 
        "OrderByDescending" && m.GetParameters().Length == 2);

        /// <summary>
        /// Gets/sets a boolean indicating whether the OrderBy is descending or ascending
        /// </summary>
        public bool Descending { get; set; }

        /// <summary>
        /// Gets/sets the raw property path
        /// </summary>
        public string PropertyPath { get; set; }

        /// <summary>
        /// Filters the specified query
        /// </summary>
        /// <param name="entityType">The type of the query</param>
        /// <param name="query">The query to filter</param>
        /// <returns>The filtered query</returns>
        public override IQueryable Filter(Type entityType, IQueryable query)
        {
            PropertyPath propertyPath;
            ParameterExpression parameterExpression;
            MemberExpression getPropertyExpression;
            LambdaExpression lambdaExpression;
            MethodInfo orderByMethod;
            MethodCallExpression filterExpression;
            //Creates the parameter expression
            parameterExpression = Expression.Parameter(entityType, "param");
            //Attempts to parse the PropertyPath
            if (!DynamicSearchesExample.PropertyPath.TryParse(this.PropertyPath, out propertyPath))
            {
                throw new Exception(string.Format
                ("Failed to parse the specified value '{0}' into a {1}", this.PropertyPath, nameof(DynamicSearchesExample.PropertyPath)));
            }
            //Creates the expression to get the value returned by the targeted property 
            //(ex: 'param.Property1.Property2')
            getPropertyExpression = propertyPath.ToExpression(parameterExpression);
            //Creates the lambda (ex: '(param) -> param.Property1.Property2')
            lambdaExpression = Expression.Lambda(getPropertyExpression, parameterExpression);
            //Check whether or not the OrderBy is descending
            if (this.Descending)
            {
                //The OrderByDescending method
                orderByMethod = OrderByFilter.OrderByDescendingMethod.MakeGenericMethod
                (entityType, getPropertyExpression.Type);
            }
            else
            {
                //The OrderBy method
                orderByMethod = OrderByFilter.OrderByMethod.MakeGenericMethod
                (entityType, getPropertyExpression.Type);
            }
            //Create the filter expression (ex: 'query.OrderBy
            //((param) -> param.Property1.Property2)')
            filterExpression = Expression.Call
            (orderByMethod, query.Expression, Expression.Quote(lambdaExpression));
            return query.Provider.CreateQuery(filterExpression);
        }
    }

正如你所看到的,我们的 OrderByFilter 采用 PropertyPath 属性。 对于那些熟悉 WPF 的人来说,它的工作方式类似。 对于其他人来说,理解它只是一个包含连续属性名称的 string 数组,在我的示例中用于生成一个 MemberExpression(例如:Client.Address.StreetName)。

3. QueryViewModel

QueryViewModel<T> 是我们将用于将变量来回传递给视图的类。 它将保存我们的 QueryFilter 实例以及一些变量,在此示例中用于对查询结果进行分页。 请注意,我们可以开发 SkipFilterTakeFilter 来获得完全相同的结果。

 /// <summary>
    /// The view model of a filterable query
    /// </summary>
    /// <typeparam name="T">The type of the query</typeparam>
    public class QueryViewModel<T>
        where T : class
    {

        private static MethodInfo CountMethod = 
        typeof(Queryable).GetMethods().Single(m => m.Name == 
        "Count" && m.GetParameters().Count() == 1);
        private static MethodInfo SkipMethod = 
        typeof(Queryable).GetMethods().Single(m => m.Name == 
        "Skip" && m.GetParameters().Count() == 2);
        private static MethodInfo TakeMethod = 
        typeof(Queryable).GetMethods().Single(m => m.Name == 
        "Take" && m.GetParameters().Count() == 2);
        private static MethodInfo ToListMethod = 
        typeof(Enumerable).GetMethod("ToList");

        /// <summary>
        /// Gets/sets the maximum results per page
        /// </summary>
        public int ResultsPerPage { get; set; }

        /// <summary>
        /// Gets/sets the current page index
        /// </summary>
        public int PageIndex { get; set; }

        /// <summary>
        /// Gets/sets the current page count
        /// </summary>
        public int PageCount { get; set; }

        /// <summary>
        /// Gets/sets the results of the query
        /// </summary>
        public IEnumerable<T> Results { get; set; }

        /// <summary>
        /// Gets/sets the 
        /// <see cref="QueryFilter"/>s associated with the query
        /// </summary>
        public IEnumerable<QueryFilter> Filters { get; set; }

        /// <summary>
        /// Executes the query represented by the 
        /// <see cref="QueryViewModel{T}"/> in the specified 
        /// <see cref="DbContext"/>
        /// </summary>
        /// <param name="context">The 
        /// <see cref="DbContext"/> to execute the query into</param>
        public void ExecuteQuery(DbContext context)
        {
            IQueryable query;
            MethodInfo countMethod, skipMethod, takeMethod, toListMethod;
            int pageCount, remainder;
            //Create the query
            query = context.Set<T>();
            if(query == null)
            {
                throw new NullReferenceException(string.Format
                ("Failed to find a {0} of the specified type '{1}'", 
                nameof(DbSet), typeof(T).Name));
            }
            if(this.Filters != null)
            {
                //Apply each filter to the query
                foreach (QueryFilter queryFilter in this.Filters)
                {
                    query = queryFilter.Filter(query.ElementType, query);
                }
            }
            //If we dont do the following, which is a nasty trick, 
            //an exception will be thrown when attempting the following Skip() call
            if (!typeof(IOrderedQueryable).IsAssignableFrom(query.Expression.Type)
                || this.Filters == null)
            {
                query = new OrderByFilter() 
                { PropertyPath = query.ElementType.GetProperties().First().Name }
                .Filter(query.ElementType, query);
            }
            countMethod = CountMethod.MakeGenericMethod(query.ElementType);
            pageCount = Math.DivRem((int)countMethod.Invoke(null, 
            new object[] { query }), this.ResultsPerPage, out remainder);
            if (remainder != 0)
            {
                pageCount++;
            }
            this.PageCount = pageCount;
            skipMethod = SkipMethod.MakeGenericMethod(query.ElementType);
            query = (IQueryable)skipMethod.Invoke(null, new object[] 
            { query, this.ResultsPerPage * this.PageIndex });
            takeMethod = TakeMethod.MakeGenericMethod(query.ElementType);
            query = (IQueryable)takeMethod.Invoke(null, new object[] 
            { query, this.ResultsPerPage });
            toListMethod = ToListMethod.MakeGenericMethod(query.ElementType);
            this.Results = (IEnumerable<T>)toListMethod.Invoke(null, new object[] 
            { query });
        }
    }

4. ModelAbstractionBinder

ModelAbstractionBinder 是一个 IModelBinder,直接从 DefaultModelBinder 继承。 借助它(以及我们的 Discriminator 属性),我们将能够将 abstract QueryFilter 类绑定到适当的实现。

 /// <summary>
    /// The <see cref="IModelBinder"/> implementation used to bind abstract classes
    /// </summary>
    /// <typeparam name="T">The type of the model to bind</typeparam>
    public class ModelAbstractionBinder<T>
        : DefaultModelBinder
    {

        public override object BindModel
        (ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            string key, discriminator;
            IEnumerable<Type> searchFilterTypes;
            Type searchFilterType;
            //Find the Discriminator value in the current request's form data
            key = controllerContext.HttpContext.Request.Form.Keys.
            OfType<string>().FirstOrDefault(k => k.Contains("Discriminator"));
            discriminator = controllerContext.HttpContext.Request.Form[key];
            if (string.IsNullOrWhiteSpace(discriminator))
            {
                //The Discriminator value is null, we therefore cannot do anything
                return base.BindModel(controllerContext, bindingContext);
            }
            //Find the loaded type that matches the Discriminator's value
            searchFilterTypes = TypeCacheUtil.FindFilteredTypes(typeof(T).Name, 
            (type) => typeof(T).IsAssignableFrom(type));
            searchFilterType = searchFilterTypes.FirstOrDefault
            (sft => sft.Name == discriminator);
            if (searchFilterType == null)
            {
                throw new NullReferenceException
                ("Failed to find a " + typeof(T).Name + 
                " with the specified discriminator '" + 
                discriminator + "'");
            }
            //Set the ModelMetadata, used by the DefaultModelBinder to do all the binding work
            bindingContext.ModelMetadata = 
            ModelMetadataProviders.Current.GetMetadataForType(null, searchFilterType);
            //Let the DefaultModelBinding do the work for us
            return base.BindModel(controllerContext, bindingContext);
        }
    }

正如你所看到的,我们使用当前请求的表单数据中包含的适当 Discriminator 字段来检索 QueryFilter 实现的 ModelMetaData

5. 使用代码

5.1. 创建一个接受 QueryViewModel<T> 作为参数的操作

        [HttpPost]
        public ActionResult Index(QueryViewModel<UserMockup> model)
        {
            if (!this.ModelState.IsValid)
            {
                return this.View(model);
            }
            model.ExecuteQuery(this.DbContext);
            return this.View(model);
        }

5.2. 创建一个视图来列出你的数据

@model DynamicSearchesExample.Models.QueryViewModel<DynamicSearchesExample.Models.UserMockup>

@{
    ViewBag.Title = "Home Page";
}

<div class="panel panel-default">
    <div class="panel-heading">Search</div>
    <div class="panel-body">
        <div class="container">
            <div class="row">
                <div class="form-group col-md-4">
                    <label>Search by</label>
                    <select class="form-control js-search-by">
                        <option value="Id">Id</option>
                        <option value="LastName">Last name</option>
                        <option value="FirstName">First name</option>
                        <option value="DateOfBirth">Date of birth</option>
                        <option value="Email">Email</option>
                        <option value="Address">Address</option>
                    </select>
                </div>
                <div class="form-group col-md-4">
                    <label>Rule</label>
                    <select class="form-control js-search-mode">
                        <option value="StringStartsWith">String starts with</option>
                        <option value="StringEndsWith">String ends with</option>
                        <option value="StringContains">String contains</option>
                        <option value="StringDoesNotContain">String does not contain</option>
                        <option value="Equals">Equals</option>
                        <option value="NotEquals">Not equals</option>
                        <option value="IsLowerThan">Is lower than</option>
                        <option value="IsLowerThanOrEquals">Is lower than or equals</option>
                        <option value="IsGreaterThan">Is greater than</option>
                        <option value="IsGreaterOrEquals">Is greater than or equals</option>
                        <option value="IsNull">Is null</option>
                        <option value="IsNotNull">Is not null</option>
                    </select>
                </div>
                <div class="form-group col-md-4">
                    <label>Value to find</label>
                    <input type="text" class="form-control js-search-value" />
                </div>
            </div>
        </div>
        <a class="form-control btn btn-primary js-search-do">Execute search</a>
        <a class="form-control btn btn-warning js-search-clear">Clear filters</a>
    </div>
</div>
<div class="panel panel-default">
    <div class="panel-body">
        @using (Html.BeginForm("index", "home", 
        FormMethod.Post, new { @class = "js-form-pagination" }))
        {
            @Html.AntiForgeryToken()
            @Html.HiddenFor(m => m.PageIndex)
            @Html.HiddenFor(m => m.PageCount)

            <div class="form-group">
                <label for="ResultsPerPage">Results per page</label>
                <select name="ResultsPerPage" class="form-control js-page-items">
                    <option value="25" @(Model.ResultsPerPage == 25 ? 
                    "selected" : "")>25</option>
                    <option value="50" @(Model.ResultsPerPage == 50 ? 
                    "selected" : "")>50</option>
                    <option value="75" @(Model.ResultsPerPage == 75 ? 
                    "selected" : "")>75</option>
                    <option value="100" @(Model.ResultsPerPage == 100 ? 
                    "selected" : "")>100</option>
                    <option value="125" @(Model.ResultsPerPage == 125 ? 
                    "selected" : "")>125</option>
                    <option value="150" @(Model.ResultsPerPage == 150 ? 
                    "selected" : "")>150</option>
                </select>
            </div>

            <div class="form-group">
                <nav aria-label="Pagination">
                    <ul class="pagination">
                        <li class="page-item @(Model.PageIndex == 0 ? 
                        "disabled" : "")">
                            <a class="page-link js-page-link" 
                            data-index="@((Model.PageIndex - 1).ToString())">Previous</a>
                        </li>
                        @for (int i = 0; i < Model.PageCount; i++)
                        {
                            <li class="page-item @(i == Model.PageIndex ? 
                            "active" : "")">
                                <a class="page-link js-page-link" 
                                data-index="@i">@((i + 1).ToString())</a>
                            </li>
                        }
                        <li class="page-item @(Model.PageIndex == 
                        Model.PageCount - 1 ? "disabled" : "")">
                            <a class="page-link js-page-link" 
                            data-index="@((Model.PageIndex + 1).ToString())">Next</a>
                        </li>
                    </ul>
                </nav>
            </div>

            int filterIndex = 0;
            if (Model.Filters != null)
            {
                foreach (DynamicSearchesExample.Models.QueryFilter filter in Model.Filters)
                {
                    <div class="js-page-filter">
                        @foreach (System.Reflection.PropertyInfo property in 
                        filter.GetType().GetProperties().Where(p => p.CanRead && p.CanWrite))
                        {
                            <input type="hidden" 
                            name="Filters[@filterIndex].@property.Name" 
                            value="@property.GetValue(filter)" />
                        }
                    </div>
                    filterIndex++;
                }
            }

            <input type="submit" class="hide" value="paginate" />
        }
    </div>
</div>
<table class="table table-bordered table-striped table-hover">
    <thead>
        <tr>
            <th class="js-page-orderby" 
            data-orderby="Id">Id</th>
            <th class="js-page-orderby" 
            data-orderby="FirstName">FirstName</th>
            <th class="js-page-orderby" 
            data-orderby="LastName">LastName</th>
            <th class="js-page-orderby" 
            data-orderby="DateOfBirth">DateOfBirth</th>
            <th class="js-page-orderby" 
            data-orderby="Address">Address</th>
            <th class="js-page-orderby" 
            data-orderby="Email">Email</th>
        </tr>
    </thead>
    <tbody>
        @foreach(DynamicSearchesExample.Models.UserMockup user in Model.Results)
        {
            <tr>
                <td>@user.Id</td>
                <td>@user.FirstName</td>
                <td>@user.LastName</td>
                <td>@user.DateOfBirth</td>
                <td>@user.Address</td>
                <td>@user.Email</td>
            </tr>
        }
    </tbody>
</table>

@section Scripts
{
    @Scripts.Render("~/scripts/searches.js")
}

5.3. 将以下脚本添加到在第 5.2 点创建的视图中

$(document).ready(function () {

    $('.js-page-link').click(function () {
        var $this = $(this);
        var $form = $('.js-form-pagination');
        var pageIndex = $this.data('index');
        var $pageIndex = $form.find('input[name="PageIndex"]');
        if ($this.parent().hasClass('disabled')) {
            return;
        }
        $pageIndex.val(pageIndex);
        $form.submit();
    });

    $('.js-page-orderby').click(function () {
        var $this = $(this);
        var orderByPath = $this.data('orderby');
        var thenByPath = $this.data('thenby');
        var descending = false;
        var $form = $('.js-form-pagination');
        var $pageIndex = $form.find('input[name="PageIndex"]');
        var $filters = $form.find('div.js-page-filter');
        var $filter = $('<div class="js-page-filter">');
        var $input = null;
        var $existingFilter = $('div.js-page-filter input[name$="Discriminator"]
        [value="OrderByFilter"]').closest('.js-page-filter');
        var existingPath, existingDescending = null;
        //Check if an orderByFilter exists
        if ($existingFilter.length > 0) {
            existingPath = $existingFilter.find('input[name$="PropertyPath"]').val();
            existingDescending = $existingFilter.find
            ('input[name$="Descending"]').val();
            descending = !existingDescending;
        }
        //Reset the page index
        $pageIndex.val(0);
        //Clear query filters
        $filters.remove();
        //Create new OrderByFilter
        $input = $('<input type="hidden" 
        name="Filters[0].Discriminator" value="OrderByFilter">');
        $filter.append($input);
        $input = $('<input type="hidden" 
        name="Filters[0].Descending" value="' + descending + '">');
        $filter.append($input);
        $input = $('<input type="hidden" 
        name="Filters[0].PropertyPath" value="' + orderByPath + '">');
        $filter.append($input);
        $form.append($filter);
        //Create new ThenByFilter if required
        if (thenByPath !== undefined) {
            $filter = $('<div class="js-page-filter">');
            $input = $('<input type="hidden" 
            name="Filters[1].Discriminator" value="ThenByFilter">');
            $filter.append($input);
            $input = $('<input type="hidden" 
            name="Filters[1].Descending" value="' + descending + '">');
            $filter.append($input);
            $input = $('<input type="hidden" 
            name="Filters[1].PropertyPath" value="' + orderByPath + '">');
            $filter.append($input);
            $form.append($filter);
        }
        //Submit the form
        $form.submit();
    });

    $('.js-page-items').on('change', function (e) {
        var $this = $(this);
        var $form = $('.js-form-pagination');
        var $pageIndex = $form.find('input[name="PageIndex"]');
        $pageIndex.val(0);
        $form.submit();
    });

    $('.js-search-do').click(function () {
        var $form = $('.js-form-pagination');
        var searchBy = $('.js-search-by').val();
        var searchValue = $('.js-search-value').val();
        var comparisonMode = $('.js-search-mode').val();
        //var caseSentitive = $('.js-search-caseSensitive').is(':checked');
        var $filters = $form.find('div.js-page-filter');
        var $filter = $('<div class="js-page-filter">');
        var $pageIndex = $form.find('input[name="PageIndex"]');
        //Reset the page index
        $pageIndex.val(0);
        //Clear query filters
        $filters.remove();
        //Create new WhereFilter
        $input = $('<input type="hidden" 
        name="Filters[0].Discriminator" value="WhereFilter">');
        $filter.append($input);
        $input = $('<input type="hidden" 
        name="Filters[0].PropertyPath" value="' + searchBy + '">');
        $filter.append($input);
        $input = $('<input type="hidden" 
        name="Filters[0].ValueComparison" value="' + comparisonMode + '">');
        $filter.append($input);
        //$input = $('<input type="hidden" 
        name="Filters[0].CaseSensitive" value="' + caseSentitive + '">');
        //$filter.append($input);
        $input = $('<input type="hidden" 
        name="Filters[0].Value" value="' + searchValue + '">');
        $filter.append($input);
        $form.append($filter);
        //Submit the form
        $form.submit();
    });

    $('.js-search-clear').click(function () {
        var $form = $('.js-form-pagination');
        var $filters = $form.find('div.js-page-filter');
        //Clear query filters
        $filters.remove();
        //Submit the form
        $form.submit();
    });
});

5.4. 瞧!

© . All rights reserved.