views:

190

answers:

1

I have a method AddStudent() which looks for a student with the same name and returns an existing student from the database if there is a student with the same name, otherwise it creates a new student and adds it to the database.

I'm curious why se = students.First<StudentEntity>(); succeeds when se = students.ElementAt<StudentEntity>(0); fails when I try to get the first result from the LINQ query. Aren't the two methods the same?

The full code for the method is shown below.

public Student AddStudent(string name)
{
    using (SchoolEntities db = new SchoolEntities())
    {
        // find student with same name via LINQ
        var students = from s in db.StudentEntitySet
                       where s.name == name
                       select s;

        StudentEntity se = default(StudentEntity);

        // if student with the same name is already present, return 
        // that student
        if (students.Count<StudentEntity>() > 0)
        {
            // if i use ElementAt, if fails with a "LINQ to Entities does not
            // recognize the method 'StudentEntity ElementAt[StudentEntity]
            // (System.Linq.IQueryable`1[StudentEntity], Int32)' method, 
            // and this method cannot be translated into a store expression.", 
            // but not when I use First. Why?

            // se = students.ElementAt<StudentEntity>(0);
            se = students.First<StudentEntity>();
        }
        else
        {
            // passing 0 for first parameter (id) since it's represented by 
            // a BigInt IDENTITY field in the database so any value
            // doesn't matter.
            se = StudentEntity.CreateStudentEntity(0, name);
            db.AddToStudentEntitySet(se);
            db.SaveChanges();
        }

        // create a Student object from the Entity object
        return new Student(se);
    }
}

Thanks!

+6  A: 

It fails because the ElementAt method is an indexed-access method and the Entity Framework doesn't know how to turn that into SQL.

When you use the First method, Entity Framework can translate this into a TOP 1 clause in a SQL query. It's very very simple. In order to use ElementAt, it would have to construct a much more complex query based on windowing functions (ROW_NUMBER()) and, well, it just isn't quite sophisticated enough to do that.

It's actually a documented limitation of the Entity Framework. The ElementAt extension simply isn't supported.


You could, in theory, write this instead:

se = students.AsEnumerable().ElementAt<StudentEntity>(0);

This instructs the Entity Framework not to try to "translate" anything after the AsEnumerable() call, so instead, it will retrieve all of the results (not just the first) and iterate through them until it gets to the element you want (which in this case just happens to be the first).

However, this will slow down the operation a lot compared to just using First(), because instead of just fetching 1 result from the server, it fetches all of them and filters afterward. I would only use this workaround if for some strange reason I needed to get the 5th or 10th element or some element other than the first one.

Aaronaught
I find it strange it couldn't do a `SKIP n TOP 1`.
Simon Buchan
@Simon: I'm not sure which DBMS you're referring to but SQL Server does not have a `SKIP` keyword. Entity Framework *can* do paging queries using the `Skip(n)` and `Take(n)` extension methods (it generates a `ROW_NUMBER` query as outlined in my answer), but the SQL translations tend to be implemented one by one, and I guess Microsoft just didn't get around to implementing `ElementAt`, they didn't think it was important enough, perhaps because it can be just as easily composed using `Skip(n)` and then `Take(1)`.
Aaronaught
+1 for the clear explanation. Thank you!
Mr Roys
Simon: as Aaronaught says you can do this by going Skip(n).Take(1).First(). I'm not sure why it doesn't turn ElementAt into this internally, but my guess is that ElementAt has different semantics if there are fewer than n results (e.g. ElementAt should throw, but Skip/Take would result in an empty sequence -- but the First would still take care of the throwing...).
itowlson
@Aaronaught: Yeah, I was meaning a `SKIP` clause in ESQL. It sounds like it's not supported by MSSQL, but most other RDBMs I've used support it (with slightly different syntax).
Simon Buchan
Compare the SQL generated by `First()` vs. `Skip(0).Take(1).First()` and you'll understand why it's better to use the correct method in the first place. :)
Craig Stuntz
@Craig: No kidding, you definitely don't want to introduce a bunch of windowing functions if you're just trying to do a `TOP 1`. Still, `First()` only works if you actually want the *first* result, not the 2nd or 3rd or nth. :)
Aaronaught