Standardizing and Abstracting IQueryable construction with Linq to Entities

十年热恋 提交于 2020-02-08 05:48:24

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!