Search code examples
c#linqsql-to-linq-conversion

How do I negotiate joins and groupings based on nested properties in LINQ?


So I've got a nested data structure like this:

public class ContractTerm
{
    public int ContractId { get; set; }
    public string SectionId { get; set; }
    public string SubsectionId { get; set; }
    public string TermId { get; set; }
    public int TermOrder { get; set; }
    public TermItem TermNavigation { get; set; }
}

public class TermItem
{
    public string SectionId { get; set; }
    public string SubsectionId { get; set; }
    public string TermId { get; set; }
    public string Text { get; set; }
    public ICollection<ContractTerm> ContractNavigation { get; set; }
}

I've also got a class to map the section/subsection pairings in a more EF-friendly way (IRL this is an enum with attribute values and a helper, but this class abstracts away some work not necessary to reproduce the issue):

public class Section
{
    public string Name { get; set; }
    public string SectionId { get; set; }
    public string SubsectionId { get; set; }
}

Both ContractTerm and TermItem have their own collections in a DbContext, and I'm trying to get a collection of all text entries assigned to specific Sections for a given ContractId. I have the following class to contain it:

public class TextsBySection
{
    public string SectionName { get; set; }
    public IEnumerable<string> Texts { get; set; }
}

I want to select a collection of TextsBySection, and have something like this:

public class ContractManager
{
    //insert constructor initializing MyContext here

    private MyContext Context { get; }

    public IEnumerable<MyOutputClass> GetTerms(int contractId, IEnumerable<Section> sections)
    {
        Func<string, string, IEnumerable<string>> getBySection =
            (section, subsection) => context.ContractTerms.Include(x => x.TermNavigation)
                                            .Where(x => x.ContractId == contractId
                                                        && x.SectionId == section
                                                        && x.SubsectionId == subsection)
                                            .Select(x => x.TermNavigation.Text);

        var result = sections.Select(x => new MyOutputClass
                              {
                                  SectionName = x.Name,
                                  Texts = getBySection(x.SectionId, x.SubsectionId)
                              }).ToList();

        return result;
    }
}

This works fine and dandy, but it hits the database for every Section. I feel like there's got to be a way to use Join and/or GroupBy to make it only query once, but I can't quite see it. Something like this, perhaps:

var result = context.ContractTerms.Include(x => x.TermNavigation)
                                  .Where(x => x.ContractId == contractId)
                                  .Join(sections,
                                        term => //something
                                        section => //something
                                        (term, section) => /*something*/)

If all this were in SQL, selecting the necessary data would be easy:

SELECT sections.name,
       term_items.text
FROM   contract_terms
JOIN   term_items
ON     term_items.section_id = contract_terms.section_id
AND    term_items.subsection_id = contract_terms.subsection_id
AND    term_items.term_id = contract_terms.term_id
JOIN   sections --not a real table; just corresponds to sections argument in method
ON     sections.section_id = contract_terms.section_id
AND    sections.subsection_id = contract_terms.subsection_id

...and then I could group the results in .NET. But I don't understand how to make a single LINQ query that would do the same thing.


Solution

  • I changed my answer, well I would do something like this... maybe this may help you.

    public static void Main(string[] args)
            {
                List<Section> sections = new List<Section>();
                List<ContractTerm> contractTerms = new List<ContractTerm>();
                List<TermItem> termItens = new List<TermItem>();
    
                //considering lists have records
    
                List<TextsBySection> result = (from contractTerm in contractTerms
                              join termItem in termItens
                                  on new
                                  {
                                      contractTerm.SectionId,
                                      contractTerm.SubsectionId,
                                      contractTerm.TermId
                                  }
                                  equals new
                                  {
                                      termItem.SectionId,
                                      termItem.SubsectionId,
                                      termItem.TermId
                                  }
                              join section in sections
                               on new
                               {
                                   contractTerm.SectionId,
                                   contractTerm.SubsectionId
                               } equals new
                               {
                                   section.SectionId,
                                   section.SubsectionId
                               }
                              select
                              new
                              {
                                  sectionName = section.Name,
                                  termItemText = termItem.Text
                              }).GroupBy(x => x.sectionName).Select(x => new TextsBySection()
                              {
                                  SectionName = x.Key,
                                  Texts = x.Select(i=> i.termItemText)
                              }).ToList();  
            }