这将产生类似于以下代码的代码。我对这段代码并不满意。有没有更好的方法可以使用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);
}
}
#1 楼
我知道这很旧,但是认为对阅读此书的任何人可能会有帮助。如果您想清理代码,则始终可以对其进行重构。.这样的代码比原始代码更具可读性:如果愿意,可以将这些方法移到服务类中。
#2 楼
编辑:我对我之前编译不充分的示例表示歉意。我已修复它,并添加了一个更完整的示例。
您可以将每个条件与更改查询的策略相关联。每个策略(在本示例中称为
SearchFieldMutator
)将具有两件事情:决定是否应用该策略的方法。
该策略本身。 />第一部分是一个委托(类型为
Predicate<TSearch>
),该委托基于true
(或任何其他类型,因为它仅定义了通用类型false
)中的数据返回UserSearchViewModel
或TSearch
。如果返回true
,则应用该策略。如果返回false
,则不适用。这是代表的类型:Predicate<TSearch>
(也可以写为
Func<TSearch, bool>
)第二部分是策略本身。它应该通过对它应用LINQ运算符来“改变”查询,但实际上只是使用添加的运算符返回了一个新查询,并且它的调用者应该丢弃旧查询并保留新查询。因此,它并不是真正的突变,但具有相同的效果。我为其创建了一个新的委托类型,因此其用法很清楚:
public delegate IQueryable<TItem> QueryMutator<TItem, TSearch>(IQueryable<TItem> items, TSearch search);
注意:我已经定义了项目类型和搜索数据为通用类型(分别为
TItem
和TSearch
),因此此代码可在代码中的多个位置使用。但是,如果这令人困惑,则可以完全删除泛型,并用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
\ $ \ 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
评论
我不确定您真正关心的是什么。该代码是可读的,至少第一遍看起来像它应该可以正常工作。很容易遵循。有时逻辑需要这种类型的复杂性,但在我看来,它看起来很干净。我可以将查询写为一行,但是有效的运行时间是相同的,但是很难理解。我希望有一些更简洁/简短的方法来进行过滤(在这种情况下为三个if语句)和排序(switch语句)。在此示例中,我仅对3个字段进行过滤和排序,但也有需要6个或更多字段的列表。很快就会导致很多代码,而且看起来很丑。您如何将其写在一行上? :-)
我可以将整行写为一行代码。只是缩短它不会使它更好。打高尔夫球的代码适合磨削技巧,但使代码易于阅读和维护则更有价值。
我编辑了第一条评论。你的逻辑不烂。有时,逻辑变得很复杂,令人讨厌。就像我说的那样,如果我将其交给我,我会很高兴。相比之下,单个命令行将包含许多替换if语句的比较。执行的最终结果查询可能相同,甚至更糟。但这将很难遵循。
哦,知道了。我同意该代码易于阅读和理解,但是编写它有点麻烦:-)我希望在不牺牲可读性的情况下对该部门进行一些改进。