0

I have a linq select part, that I use quite often so I wondered, if there is a way to "reuse" it, i.e. put it in a variable of some sort.

My linq statement looks a bit like this:

var today = DateTime.Now;
var tmp = _context.Student
.Where(b => b.Semester > 10)
.Select(b => new {
    BName = b.Name,
    SuperList = b.Seminars.SelectMany(sem => sem.DateFrom <= today && ......) // this is a superfancy and long subquery that I wanna reuse
});

Since I have some other linq queries that will use exactly the same subquery, I tried to create a function that gives me this subquery part for reusing. But alas, it doesnt work, the query does not throw an error but it looks as if the subquery just was completely ignored:

var today = DateTime.Now;
var f1 = GetFancySubquery(today);

var tmp = _context.Student.Where(b => b.Semester > 10)
   .Select(b => new {
        BName = b.Name,
        SuperList = f1.Invoke(b)
    });       

private Func<Student, List<SemesterSomethingDTO>> GetFancySubquery(DateTime tag)
{
    Func<Student, List<SemesterSomethingDTO>> condition = s => s.Seminars.SelectMany(sem => ....).ToList();
    return condition;
}

I feel like I'm close (or maybe not) - what am I doing wrong? Is what I'm trying to do even possible?

6
  • A query is ignored unless it is actually used in the code. Commented Jan 24, 2022 at 14:34
  • 3
    GetFancySubQuery should return an IQueryable<>, then you can append further modifications (such as .Where or .OrderBy), before your final projection. Adding .ToList will execute the command, so remove that bit. Commented Jan 24, 2022 at 14:37
  • 1
    Note that queries versus EF are not executed. They set up an expression that will be translated to SQL. This SQL will then be executed. Therefore this Invoke() will not work as expected. E.g. conditions must be formulated as Expression<Func<T, bool>>. But what you call "condition" is in fact a projection. Commented Jan 24, 2022 at 14:54
  • Thank you! but how would I use this using Expressions ? do you have any hints as to how I could achieve what I want? Commented Jan 24, 2022 at 15:05
  • 1. What LINQ are you using: EF 6.x / EF Core 2.0 / 2.1 / 3.x / 5.x / 6.x? 2. What other conditions are in the SelectMany - i.e. do any of them reference b? 3. Your lambda to SelectMany appears to return a boolean which is invalid? Commented Jan 24, 2022 at 19:03

4 Answers 4

1

I feel like I'm close (or maybe not) - what am I doing wrong? Is what I'm trying to do even possible?

You were close indeed, and yes it's possible.

I would think the best approach for you case it to create an extension method for IEnumerable<Student> (the list you want to query), since you'll be re-using query in several parts, and that's why extension methods can help you keep things D.R.Y.

It would be something like this:

class Program
{
    static void Main(string[] args)
    {
        var today = DateTime.Now;
        List<Student> students = new List<Student>();

        // Not quite the same code as yours since I don't have all the classes
        // but it's the same idea
        var tmp = students
            .Where(b => b.Name == "Student 1")
            .Select(b => new
            {
                BName = b.Name,
                SuperList = b.Courses.Select(course => course.Semester == 2).ToList()
            });

        // This is exactly the same query using and extension method
        var tmp2 = students.FancyQuery("Student 1", 2);
    }
}

public static class LinqExtension
{
    /// <summary>
    /// Extension method to query an IEnumerable<Student> by name and course semester
    /// </summary>
    /// <param name="query">Type that the method extends</param>
    /// <param name="name">Student name</param>
    /// <param name="semester">Course semester</param>
    /// <returns>IEnumerable<QuerySelectResult> after filtering by name and semester</student></returns>
    public static List<QuerySelectResult> FancyQuery(this IEnumerable<Student> query, string name, int semester)
    {
        return query.Where(b => b.Name == name)
                .Select(b => new QuerySelectResult
                {
                    BName = b.Name,
                    SuperList = b.Courses.Where(course => course.Semester == 2).ToList()
                }).ToList();
    }
}

public class Student
{
    public string Name { get; set; }
    public List<Course> Courses { get; set; }
}

public class Course
{
    public string Name { get; set; }
    public int Semester { get; set; }
}

public class QuerySelectResult
{
    public string BName { get; set; }
    public List<Course> SuperList { get; set; }
}

Notes:

  1. Extension classes and methods within must be public static to work. AFAIK.
  2. this IEnumerable<Student> argument on LinqExtension's FancyQuery method, refers to the type the extension is for. For example, you won't be able to use it with an IEnumerable<int>.
  3. Code isn't the exactly the same as yours but tried to recreate a similar situation.
  4. LinqExtension shouldn't be in the same class as my code sample. Should be for example in Extension or Helper namespace (folder). To use it anywhere on you code, you should insert an using statement with it's namespace and it becomes available to extend the type you are extending

If something isn't clear, please do ask :)

Hope it's useful.

EDIT 1: Now the extension method doesn't have the whole query (Where and Select clauses).

Thank you very much! The problem here is: fancyQuery contains now the entire query. While I want only the subquery as reusable, the rest can look very different. so unfortunately this doesnt help me

how? it seems like I'm missing an important puzzlepiece here - how do I do it ?

class Program
{
    static void Main(string[] args)
    {
        var today = DateTime.Now;
        List<Student> students = new List<Student>();

        // Not quite the same code as yours since I don't have all the classes
        // but it's the same idea
        var tmp = students
            .Where(b => b.Name == "Student 1")
            .Select(b => new
            {
                BName = b.Name,
                SuperList = b.Courses.Select(course => course.Semester == 2).ToList()
            });

        // As you can see "students.Where(b => b.Name == "Student 1")" it's still and IEnumerable after the Where()
        // clause is used, so the extension method can be used after it
        var tmp2 = students.Where(b => b.Name == "Student 1").FancyQuery(2);
    }
}

public static class LinqExtension
{
    /// <summary>
    /// Extension method to query an IEnumerable<Student> by name and course semester
    /// </summary>
    /// <param name="query">Type that the method extends</param>
    /// <param name="name">Student name</param>
    /// <param name="semester">Course semester</param>
    /// <returns>IEnumerable<QuerySelectResult> after filtering by name and semester</student></returns>
    public static List<QuerySelectResult> FancyQuery(this IEnumerable<Student> query, int semester)
    {
        // Took out the Where() clause from here, and left only the Select() one
        return query.Select(b => new QuerySelectResult
        {
            BName = b.Name,
            SuperList = b.Courses.Where(course => course.Semester == semester).ToList()
        }).ToList();

    }
}

EDIT 2: Extension method only for SuperList (IEnumerable<Course> with my classes)

but fancyQuery adds the entire select - which I do not want. maybe next time I do not need BName but wanna query something else - the only thing I need is SuperList

You can extend whatever type you want, just changed the the return and signature of the extension method. Now it's only used on SuperList, but can be used with any object of type IEnumerable<Course> (in this case)

static void Main(string[] args)
    {
        var today = DateTime.Now;
        List<Student> students = new List<Student>();

        // Not quite the same code as yours since I don't have all the classes
        // but it's the same idea
        var tmp = students
            .Where(b => b.Name == "Student 1")
            .Select(b => new
            {
                BName = b.Name,
                SuperList = b.Courses.Select(course => course.Semester == 2).ToList()
            });

        // Now the extension method is used only on SuperList 
        var tmp2 = students
            .Where(b => b.Name == "Student 1")
            .Select(b => new
            {
                BName = b.Name,
                SuperList = b.Courses.FancyQuery(2)
            });
    }
}

public static class LinqExtension
{
    /// <summary>
    /// Extension method to query an IEnumerable<Student> by name and course semester
    /// </summary>
    /// <param name="query">Type that the method extends</param>
    /// <param name="name">Student name</param>
    /// <param name="semester">Course semester</param>
    /// <returns>IEnumerable<QuerySelectResult> after filtering by name and semester</student></returns>
    public static List<Course> FancyQuery(this IEnumerable<Course> query, int semester)
    {
        // Took out the Where() clause from here, and left only the Select() one
        return query.Where(course => course.Semester == semester).ToList();

    }
}
Sign up to request clarification or add additional context in comments.

9 Comments

Thank you very much! The problem here is: fancyQuery contains now the entire query. While I want only the subquery as reusable, the rest can look very different. so unfortunately this doesnt help me
It's the same, just use the subquery on the FancyMethod, it will still be an IEnumerable<Student> so it can be used on any part of the query.
how? it seems like I'm missing an important puzzlepiece here - how do I do it ?
Made the edit, hope it's understandable now :)
but fancyQuery adds the entire select - which I do not want. maybe next time I do not need BName but wanna query something else - the only thing I need is SuperList
|
1

Such helper functions needs expanding into resulting Expression Tree. It is not supported by vanilla EF any versions. I would suggest to use use LINQKit for such task:

public static class DTOExtensions
{
    [Expandable(nameof(GetFancySubqueryImpl))]
    public static List<SemesterSomethingDTO> GetFancySubquery(this Student student, DateTime tag)
    {
        throw new InvalidOperationException();
    }

    private static Expression<Student, DateTime, List<SemesterSomethingDTO>> GetFancySubqueryImpl()
    {
        return (student, tag) => student.Seminars.SelectMany(sem => ....).ToList();
    }
}

And usage:

var today = DateTime.Now;
var tmp = _context.Student
    .Where(b => b.Semester > 10)
    .AsExpandable() // activating LINQKit
    .Select(b => new {
        BName = b.Name,
        SuperList = b.GetFancySubquery(today)
    });

Comments

0

I don't have any idea about your entites. But you can try belong code

var today = DateTime.Now;
var tmp = _context.Student.Include(i=>i.SuperList).Where(b => b.Semester > 10)
.Select(b => new {
   BName = b.Name,
   SuperList = b.Seminars.Where(sem => sem.DateFrom <= today && ......) 
});

1 Comment

SuperList is not an Entity, SuperList is a result of a SelectMany with some joins etc - which also means it would take a shitload of includes which makes the whole thing obsolete - is there no other way?
-1

It looks like you are missing the .Include statement to ensure the seminars data is loaded.

Does it work if you try:

_context.Student.Include(s => s.Seminars)... // continue your query here

You can read more about .Include here.

Also, you can declare your Func delegate as a variable instead of returning it from a function. Consider this example from Microsoft's documentation:

// Declare a Func variable and assign a lambda expression to the
// variable. The method takes a string and converts it to uppercase.
Func<string, string> selector = str => str.ToUpper();

// Create an array of strings.
string[] words = { "orange", "apple", "Article", "elephant" };
// Query the array and select strings according to the selector method.
IEnumerable<String> aWords = words.Select(selector);

4 Comments

Thanks but if that is the problem then I would need a lot of includes for this to work, which would make the whole thing obsolete (reusable select but a long list of includes instead) - is there no other way?
@Chi - The first question is does this fix it? It may not resolve the problem. Also, your final select statement may need to come after a ToList() call so that you are working with the result set as C# objects in memory instead of on the database. It is possible that what you are doing to build your SuperList can't be done on the database via EF and has to be done after the query has been run.
@Chi - You could likely write a Func do perform all of these includes, as well. Or you could write a function that runs this specific query where the context and the filter criteria are passed in to the function. That could reduce the code repetition.
yes, with include it works! but now I have like 8 includes which of course is bad too

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.