我正在使用ASP.NET MVC 3和Entity Framework 4.1开发应用程序。在该应用程序中,我有很多页面列表。用户可以对这些列表进行过滤和排序。

这将产生类似于以下代码的代码。我对这段代码并不满意。有没有更好的方法可以使用Entity Framework进行过滤和排序?

有些人可能建议将代码放入服务类而不是控制器中,但这只会将难看的代码移到某个地方其他。我最终会在服务中得到丑陋的代码,而不是控制器中的丑陋代码。

public UsersController : Controller
{
    private const int PageSize = 25;

    public ActionResult Index(int page = 1, string sort = "", UserSearchViewModel search)
    {
        // Get an IQueryable<UserListItem>
        var users = from user in context.Users
                    select new UserListItem
                    {
                        UserId = user.UserId,
                        Email = user.Email,
                        FirstName = user.FirstName,
                        LastName = user.LastName,
                        UsertypeId = user.UsertypeId,
                        UsertypeDescription = users.Usertype.Description,
                        UsertypeSortingOrder = users.Usertype.SortingOrder
                    };

        // Filter on fields when needed
        if (!String.IsNullOrWhiteSpace(search.Name)) users = users.Where(u => u.FirstName.Contains(search.Name) || u.LastName.Contains(search.Name));
        if (!String.IsNullOrWhiteSpace(search.Email)) users = users.Where(u => u.Email.Contains(search.Email));
        if (search.UsertypeId.HasValue) users = users.Where(u => u.UsertypeId == search.UsertypeId.Value);

        // Calculate the number of pages based on the filtering
        int filteredCount = users.Count();
        int totalPages = Convert.ToInt32(Math.Ceiling((decimal)filteredCount / (decimal)PageSize));

        // Sort the items
        switch(sort.ToLower())
        {
            default:
                users = users.OrderBy(u => u.FirstName).ThenBy(u => u.LastName);
                break;
            case "namedesc":
                users = users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName);
                break;
            case "emailasc":
                users = users.OrderBy(u => u.Email);
                break;
            case "emaildesc":
                users = users.OrderByDescending(u => u.Email);
                break;
            case "typeasc":
                users = users.OrderBy(u => u.UsertypeSortingOrder);
                break;
            case "typedesc":
                users = users.OrderByDescending(u => u.UsertypeSortingOrder);
                break;
        }

        // Apply the paging
        users = users.Skip(PageSize * (page - 1)).Take(PageSize);

        var viewModel = new UsersIndexViewModel
                        {
                            Users = users.ToList(),
                            TotalPages = totalPages
                        };

        return View(viewModel);
    }
}


评论

我不确定您真正关心的是什么。该代码是可读的,至少第一遍看起来像它应该可以正常工作。很容易遵循。有时逻辑需要这种类型的复杂性,但在我看来,它看起来很干净。我可以将查询写为一行,但是有效的运行时间是相同的,但是很难理解。

我希望有一些更简洁/简短的方法来进行过滤(在这种情况下为三个if语句)和排序(switch语句)。在此示例中,我仅对3个字段进行过滤和排序,但也有需要6个或更多字段的列表。很快就会导致很多代码,而且看起来很丑。您如何将其写在一行上? :-)

我可以将整行写为一行代码。只是缩短它不会使它更好。打高尔夫球的代码适合磨削技巧,但使代码易于阅读和维护则更有价值。

我编辑了第一条评论。你的逻辑不烂。有时,逻辑变得很复杂,令人讨厌。就像我说的那样,如果我将其交给我,我会很高兴。相比之下,单个命令行将包含许多替换if语句的比较。执行的最终结果查询可能相同,甚至更糟。但这将很难遵循。

哦,知道了。我同意该代码易于阅读和理解,但是编写它有点麻烦:-)我希望在不牺牲可读性的情况下对该部门进行一些改进。

#1 楼

我知道这很旧,但是认为对阅读此书的任何人可能会有帮助。如果您想清理代码,则始终可以对其进行重构。.这样的代码比原始代码更具可读性:

如果愿意,可以将这些方法移到服务类中。

#2 楼


编辑:我对我之前编译不充分的示例表示歉意。我已修复它,并添加了一个更完整的示例。


您可以将每个条件与更改查询的策略相关联。每个策略(在本示例中称为SearchFieldMutator)将具有两件事情:


决定是否应用该策略的方法。
该策略本身。 />第一部分是一个委托(类型为Predicate<TSearch>),该委托基于true(或任何其他类型,因为它仅定义了通用类型false)中的数据返回UserSearchViewModelTSearch。如果返回true,则应用该策略。如果返回false,则不适用。这是代表的类型:

Predicate<TSearch>


(也可以写为Func<TSearch, bool>

第二部分是策略本身。它应该通过对它应用LINQ运算符来“改变”查询,但实际上只是使用添加的运算符返回了一个新查询,并且它的调用者应该丢弃旧查询并保留新查询。因此,它并不是真正的突变,但具有相同的效果。我为其创建了一个新的委托类型,因此其用法很清楚:

public delegate IQueryable<TItem> QueryMutator<TItem, TSearch>(IQueryable<TItem> items, TSearch search);



注意:我已经定义了项目类型和搜索数据为通用类型(分别为TItemTSearch),因此此代码可在代码中的多个位置使用。但是,如果这令人困惑,则可以完全删除泛型,并用TItem替换任何UserListItem并用TSearch替换任何UserSearchViewModel。现在,我们已经定义了两种策略类型,可以创建一个既包含它们又可以进行突变的类:

public class SearchFieldMutator<TItem, TSearch>
{
    public Predicate<TSearch> Condition { get; set; }
    public QueryMutator<TItem, TSearch> Mutator { get; set; }

    public SearchFieldMutator(Predicate<TSearch> condition, QueryMutator<TItem, TSearch> mutator)
    {
        Condition = condition;
        Mutator = mutator;
    }

    public IQueryable<TItem> Apply(TSearch search, IQueryable<TItem> query)
    {
        return Condition(search) ? Mutator(query, search) : query;
    }
}


该类同时包含条件和策略本身,并通过使用Apply()方法,如果条件满足,我们可以轻松地将其应用于查询。

现在我们可以创建策略列表。我们将定义一些位置来保存它们(在您的一个类中),因为它们只需创建一次(毕竟它们是无状态的):

List<SearchFieldMutator<UserListItem, UserSearchViewModel>> SearchFieldMutators { get; set; }
然后,我们将填充列表:

SearchFieldMutators = new List<SearchFieldMutator<UserListItem, UserSearchViewModel>>
{
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => !string.IsNullOrWhiteSpace(search.Name), (users, search) => users.Where(u => u.FirstName.Contains(search.Name) || u.LastName.Contains(search.Name))),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => !string.IsNullOrWhiteSpace(search.Email), (users, search) => users.Where(u => u.Email.Contains(search.Email))),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => search.UsertypeId.HasValue, (users, search) => users.Where(u => u.UsertypeId == search.UsertypeId.Value)),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => search.CurrentSort.ToLower() == "namedesc", (users, search) => users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName)),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => search.CurrentSort.ToLower() == "emailasc", (users, search) => users.OrderBy(u => u.Email)),
    // etc...
};


然后我们可以尝试在查询上运行它。我将使用一个简单的UserListItem数组,而不是实际的Entity Framework查询,并在其上添加一个.ToQueryable()。如果将其替换为实际的数据库查询,它将起到相同的作用。为了示例,我还将创建一个简单的搜索:

// This is a mock EF query.
var usersQuery = new[]
{
    new UserListItem { FirstName = "Allon", LastName = "Guralnek", Email = null, UsertypeId = 7 },
    new UserListItem { FirstName = "Kristof", LastName = "Claes", Email = "whoknows@example.com", UsertypeId = null },
    new UserListItem { FirstName = "Tugboat", LastName = "Captain", Email = "tugboat@ahoy.yarr", UsertypeId = 12 },
    new UserListItem { FirstName = "kiev", LastName = null, Email = null, UsertypeId = 7 },
}.AsQueryable();

var searchModel = new UserSearchViewModel { UsertypeId = 7, CurrentSort = "NameDESC" };


下面实际上完成了所有工作,它更改了usersQuery变量内的查询到所有搜索策略指定的一种:

foreach (var searchFieldMutator in SearchFieldMutators)
    usersQuery = searchFieldMutator.Apply(searchModel, usersQuery);


就这样!这是查询的结果:



您可以尝试自己运行它。这是一个LINQPad查询,供您使用:

http://share.linqpad.net/7bud7o.linq

评论


\ $ \ begingroup \ $
这应该是公认的答案。但是,发布的代码无法编译,这可能就是为什么它没有更多投票的原因,并且由于涉及的泛型,可能成为许多投票的障碍。
\ $ \ endgroup \ $
–拖船船长
15年3月24日在17:01

\ $ \ begingroup \ $
有人成功实现了这种方法吗?该行炸弹在T1> QueryMutator 中委托IQueryable 项,T1条件); CS1960 C#无效的方差修饰符。只能将接口和委托类型参数指定为变量。
\ $ \ endgroup \ $
–基辅
15年8月31日在17:36

\ $ \ begingroup \ $
@kiev:我已经修复了代码,使其合规(加上有效的示例)。我还添加了更详细的说明。
\ $ \ endgroup \ $
– Allon Guralnek
2015年10月11日在8:04

\ $ \ begingroup \ $
这是我见过的最优雅的解决方案。我希望我可以投票数千次。
\ $ \ endgroup \ $
–哈桑·塔雷克(Hassan Tareq)
19年8月22日在15:00

#3 楼

可以以更具说明性的语法来实现排序功能。首先将关联词典声明为该类的私有成员。

    private Dictionary<string, Func<IQueryable<UserListItem>, IQueryable<UserListItem>>> _sortAssoc = new Dictionary<string, Func<IQueryable<UserListItem>, IQueryable<UserListItem>>>(StringComparer.OrdinalIgnoreCase)
    {
        { default(string),      users => users.OrderBy(u => u.FirstName).ThenBy(u => u.LastName)},
        { "namedesc",           users => users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName)} ,
        { "emailasc",           users => users.OrderBy(u => u.Email) },
        { "emaildesc",          users => users.OrderByDescending(u => u.Email) },
        //...           
    };


,然后您可以通过以下方式调用合适的排序方法: >

#4 楼

如果您使用EF 4.1的ADO.NET实体框架生成器,则可以像下面那样编写代码。

方法是构造一个排序字符串。 “按个人名称asc排序”将如下所示
“ it.personname asc”-EF内部使用“ it”。

仅用于ADO.NET EF生成器。 EF 4.3.1的DBcontext不支持此功能。

#5 楼

我会考虑使用EntitySQL代替LINQ to Entities。使用EntitySQL,您可以使用语句将排序字段名称连接起来。尽管没有编译时检查有很大的缺点。

#6 楼

另一个选择:


创建一个域对象,该域对象使用过滤器所需的参数

在存储库中创建一个函数,以使用该过滤器域对象,并在其中存储库函数处理过滤/排序/分页
从上层(无论是业务层/服务还是MVC控制器)调用该函数,并传入过滤器域对象

优点:


如果您想扩大过滤器,则只需添加到“过滤器”域对象
并修改存储库以处理该新过滤器

在其他任何要获取该组数据的地方,调用方只需要在这种情况下根据需要填充过滤器

无论如何,这已经证明是处理此问题的最佳方法考虑可伸缩性的情况,并减少重复代码的机会。

#7 楼

我也写了很多这样的代码...

我已经读过有关“动态LINQ”的信息,但是当您需要orderby()时,这并不能解决问题。thenby( )。我做过的唯一不同的事情是使用枚举来表示字段名称和排序方向,这在使用ViewState的WebForms应用程序中很好用,但是我不确定如何在MVC中维护“排序状态”;)

如果表很大,您可以考虑在存储过程中进行服务器端分页,而不是先返回整个数据库,然后在本地对其进行排序,但这是另一个主题:)

评论


\ $ \ begingroup \ $
哦,实体框架的延迟执行可确保分页在服务器端完成。实际上,仅当我执行users.ToList()时才对数据库进行调用。 (也会对users.Count()进行调用)
\ $ \ endgroup \ $
–克里斯托夫·克莱斯(Kristof Claes)
2011年7月21日在8:16