简体   繁体   中英

Entity framework, get surrounding items of the selected item

I want to get an item from db, and items that come before and after it.

var data= repo .OrderBy(a => a.Date)
               .Select((item, index) => new { item, index })
               .Where(itemAndIndex=>itemAndIndex.item.Id == someId)

this is what I got so far.

To clarify,

lets say this is my Table

Id     Name        Date
1      SomeText1   01.01.2017
2      SomeText2   03.01.2017
3      SomeText3   02.01.2017
4      SomeText4   04.01.2017
5      SomeText5   05.01.2017

I want to do a select query for Id==3 , then sort the result by the Date field, and get a list of items

Id     Name        Date
1      SomeText1   01.01.2017
3      SomeText3   02.01.2017
2      SomeText2   03.01.2017

like this.

Thank you.

Here is the same idea as in Harald Coppoolse's answer (which I think is the only reasonable way to satisfy your requirement), but with best (IMO) LINQ to Entities SQL query translation (assuming repo is IQueryable<T> representing your table):

var data = repo
    .Where(elem => elem.Id == someId)
    .SelectMany(elem =>
        repo.Where(e => e.Date < elem.Date).OrderByDescending(e => e.Date).Take(1)
        .Concat(new[] { elem })
        .Concat(repo.Where(e => e.Date > elem.Date).OrderBy(e => e.Date).Take(1)))
    .ToList();

The simplest way:

var data = repo.OrderBy(x => x.Date)
    .Select((item, index) => new { item, index });

var idx = data.First(x => x.item.Id == someId);

var result = data
    .Where(x => x.index >= idx.index)
    .Take(3);

This is not for EF , but will also work with slight modifications:

public class Data
{
    public int id;
    public string name;
    public DateTime date;
}

class Program
{
    static void Main(string[] args)
    {
        List<Data> l = new List<Data>
        {
            new Data { id = 1, name = "Name1", date = DateTime.Parse("2017/01/01")},
            new Data { id = 2, name = "Name2", date = DateTime.Parse("2017/01/03")},
            new Data { id = 3, name = "Name3", date = DateTime.Parse("2017/01/02")},
            new Data { id = 4, name = "Name4", date = DateTime.Parse("2017/01/04")},
            new Data { id = 5, name = "Name5", date = DateTime.Parse("2017/01/05")},
        };

        int id = 2;

        var result = l.Where(c => c.id == id)
            .Union(l.Where(c => c.date < l.Where(t => t.id == id).Select(d => d.date).First()).OrderByDescending(c => c.date).Take(1))
            .Union(l.Where(c => c.date > l.Where(t => t.id == id).Select(d => d.date).First()).OrderBy(c => c.date).Take(1)).ToList();
    }
}

another The simplest way:

var data= repo.OrderBy(c => c.Date)
           .Where(c => c.Id == id || c.Id == id - 1 || c.Id == id + 1)
           .Select(c => new { c.Id, c.Name,c.Date });

You forgot to mention what should be filled if your item with someId is the first or the last in your ordered list in which case you don't have a previous or next item.

And what do you want if there is no item with someId? an empty sequence or a sequence with three NULL values?

Furthermore, are your date values unique? What do you want as previous item and next item if you have several items with the same date?

Setting these problems aside, looking at your problem, from your source sequence of RepoElements, you want to select a sequence of 3 RepoElements, where

  • result 1 = the element from the source sequence with Id = someId. This is the middle element of your sequence.
  • result[0] = the element from the source sequence with the largest Date < result 1 .Date.
  • result[2] = the element from the source sequence with the smallest Data > result 1 .Date

From your repo sequence, use a Where to select the result 1 . After that use a Select with Min and Max to find result[0] and result[2] and put all three results in an array. Finally use SingleOrDefault to select the one and only element with the correct result 1 :

var result = repo
    .Where(repoElement => repoElement.Id == someId)
    .Select( result1 => new RepoElement[]
    {
        // [0]: the largest element with date < date of item with Id == someId
        repo.Where(repoElement => repoElement.Date < result1.Date)
            .Max(repoElement => repoElement.Date),
        // [1] the item with Id == someId
        result1,
        // [2] the smallest element date > date of item with Id == someId
        repo.Where(repoElement => repoElement.Date > result1.Date)
            .Min(repoElement => repoElement.Date),
    })
    .SingleOrDefault();

The first Where is done using your index: fast. The Select needs two passes: one to find the largest element that is smaller than your middle element and one to find the smallest element that is larger than your middle element. Still I think in most cases that will be faster than a sort

Note that if there are no elements with smaller / larger dates, then the result of Max will be default, which in your case is NULL

If your Date is not unique, you will find the largest cq smallest value that does not have date.

Addition

I just realized that function Max that I used does not return the element with the Max value, but the Maximum value itself. Luckily MSDN Enumerable.Max describes that the function Max uses IComparable.

Consider to let your class would implement IComparable, which would rank by Date.

private class RepoElement: IComparable<MyData>
{
    public int Id {get; set;}
    public string Name {get; set;}
    public DateTime Date {get; set;}

    public int CompareTo(RepoElement other)
    {   // this object smaller than other object
        // if this date smaller than other date
        return this.Date.CompareTo(other.Date);
    }
}

var result = repo
    .Where(repoElement => repoElement.Id == someId)
    .Select( result1 => new RepoElement[]
    {
        repo.Where(repoElement => repoElement.Date < result1.Date).Max(),
        repoElement,
        repo.Where(repoElement => repoElement.Date > result1.Date).Min(),
    }

This would use extension function Enumerable.Max() which returns a TSource.

Although this would work, this method has the disadvantage that it only works on IEnumerable, not on IQueryable. It can't be performed on the database side.

If your collection is very large, and you really need to perform it as an IQueryable (on the database side), consider using Enumerable.Aggregate to find elements [0] and [2].

Aggregate compares the two first elements of the sequence, decides which one is the "best", and uses this "best" one to compare with the third item to decide which one is the "best" again and use this one to compare with the fourth item, etc. Finally it returns the "best".

 var result = repo
    .Where(repoElement => repoElement.Id == someId)
    .Select(repoElement => new RepoElement[]
    {
        // [0]: take only the elements that are smaller,
        // aggregate to find the largest one:
        repo.Where(element => element.Date < repoElement.Date)
            .Aggregate(
            // compare the largest item already found, with the current one
            // and take the largest one as largest
            (largest, next) => next.Date > largest.Date ? next : largest),
        // [1]
        repoElement,
        // [2]: take only the elements that are bigger
        // aggregate to find the smallest one
        repo.Where(element => element.Date > repoElement.Date)
            .Aggregate(
            // compare the smallest item already found, with the current one
            // and take the smallest one as smallest
            (smallest, next) => next.Date < smallest.Date ? next : smallest),
    })
    .SingleOrDefault();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM