问题
TL;DR; What is the best way to standardize and abstract IQueryable construction in large data access layers? Are extensions acceptable or encouraged?
Background
We're using Entity Framework 6 with a Repository pattern as our Data Access Layer.
In order to make our data calls more efficient, we've recently started using some structured Data Transfer Objects to force ourselves to only pull from the database what is necessary.
For example: We have a dashboard that uses 15 of 500 properties of an Entity mapped Database Table to create a paged summary of Profiles.
Instead of pulling the full entity and mapping in a convert, we're casting right from a SELECT
statement:
//This is a simplified representation
public List<PersonDashboardDTO> GetPeopleByRangeForDashboard(int start, int length)
{
var returnPeople = new List<PersonDashboardDTO>();
IQueryable<PersonForDashboardDTO> People = databaseContext.Profile
.Where(x => !x.IsDeleted)
.OrderByDescending(x => x.LastName)
.Skip(start).Take(length)
.Select(y => new PersonForDashboardDTO
{
Name = String.Concat(y.FirstName, " ", y.LastName),
Company = y.CompanyContact.Select(x => x.Company.Name).FirstOrDefault(),
SummaryAddress = y.Address.AddressLine1,
City = y.Address.City,
IsEmailOK = y.Notifications.CanSendEmail,
});
returnPeople.AddRange(People);
return returnPeople;
}
While this is a simple example, some of these SELECT
mappings are more than 150 properties, and it goes against every grain of my being to simply copy and paste it over and over again.
It also seems to stand to reason, that since IQueryable
doesn't execute until it is cast to another object (like .ToList(), or List.AddRange(IQueryable<>)
that we could create methods to abstract Data Access calls in a more structured manner.
I'm not sure what the right pattern is, but here is what I'm thinking:
Proposal: Extension Methods
public static IQueryable<PersonDashboardDTO> MapToPersonDashboardDTO(this IQueryable<Profile> profile)
{
return profile.Select(y => new PersonDashboardDTO
{
Name = String.Concat(y.FirstName, " ", y.LastName),
Company = y.CompanyContact.Select(x => x.Company.Name).FirstOrDefault(),
SummaryAddress = y.Address.AddressLine1,
City = y.Address.City,
IsEmailOK = y.Notifications.CanSendEmail
});
}
public static IQueryable<Profile> IsNotDeleted(this IQueryable<Profile> profile)
{
return profile.Where(x => !x.IsDeleted);
}
public static IQueryable<Profile> OrderedByLastName(this IQueryable<Profile> profile)
{
return profile.OrderByDescending(x => x.LastName);
}
public static IQueryable<Profile> TakeRange(this IQueryable<Profile> profile, int start, int length)
{
return profile.Skip(start).Take(length);
}
Sample Implementation
public List<PersonDashboardDTO> GetPeopleByRangeForDashboard(int start, int length)
{
var returnPeople = new List<PersonDashboardDTO>();
IQueryable<PersonDashboardDTO> People = databaseContext.Profile
.IsNotDeleted()
.OrderedByLastName()
.TakeRange(start, length)
.MapToPersonDashboardDTO();
returnPeople.AddRange(People);
return returnPeople;
}
Summary
Is this an acceptable and usable pattern to use at large to standardize the queries we make with EF6? It seems like a good way to go, but I can't find much in the way of standards and practices here and would love some fresh eyes.
回答1:
Seems reasonable to me, but I would make the following changes:
public IQueryable<PersonDashboardDTO> GetPeopleByRangeForDashboard(int start, int length)
{
return databaseContext.Profile
.IsNotDeleted()
.OrderedByLastName()
.TakeRange(start, length)
.MapToPersonDashboardDTO();
}
No reason for the DAL to convert the result into a List instead of just returning IQueryable in most cases, and if your application doesn't require certain fields of the object, it could/should recast to a lesser complete object before enumerating it. This will result in faster database access in most cases and in some cases quite dramatically. Especially if a field like Address isn't used, then the database can drop the join.
public static IQueryable<T> TakeRange<T>(this IQueryable<T> profile, int start, int length)
{
return profile.Skip(start).Take(length);
}
No reason for this to be attached to only a query that returns profiles. This will work on any IQueryable.
来源:https://stackoverflow.com/questions/29104544/standardizing-and-abstracting-iqueryable-construction-with-linq-to-entities