How to assign multiple LINQ Include() statements to a variable for code re-use?

Viewed 234

I would like to store a set of .include() statements in a variable since I need to use the same includes in multiple locations. So in an attempt to prevent code duplication, here is what I am asking:

For example, take this following query where I eager load several related entities:

var record = await db.DisplacementDamageRecords
        .Include(tr => tr.Mission)
            .ThenInclude(m => m.MissionRadiationPartsLead)
        .Include(tr => tr.Mission)
            .ThenInclude(m => m.MissionInstruments)
                .ThenInclude(mi => mi.Instrument)
        .Include(tr => tr.Mission)
            .ThenInclude(m => m.MissionInstruments)
                .ThenInclude(mi => mi.CognitiveEngineer)
        .Include(tr => tr.ProcurementPart)
            .ThenInclude(pp => pp.Part)
        .FirstOrDefaultAsync(tr => tr.Id == form.Id);

It would be useful to extract the includes like so:

var testRecordIncludes = 
        .Include(tr => tr.Mission)
            .ThenInclude(m => m.MissionRadiationPartsLead)
        .Include(tr => tr.Mission)
            .ThenInclude(m => m.MissionInstruments)
                .ThenInclude(mi => mi.Instrument)
        .Include(tr => tr.Mission)
            .ThenInclude(m => m.MissionInstruments)
                .ThenInclude(mi => mi.CognitiveEngineer)
        .Include(tr => tr.ProcurementPart)
            .ThenInclude(pp => pp.Part)

So that I could then use this variable like so:

var record = await db.DisplacementDamageRecords
         .testRecordIncludes()
         .FirstOrDefaultAsync(tr => tr.Id == form.Id);

Is this possible? Thanks!

2 Answers

You could define an extension method like this:

 public static class DbExtensions
 {
     public static IQueryable<DisplacementDamageRecord> TestRecordIncludes(this IQueryable<DisplacementDamageRecord> records)
     {
         return records
                  .Include(tr => tr.Mission)
                      .ThenInclude(m => m.MissionRadiationPartsLead)
                  .Include(tr => tr.Mission)
                      .ThenInclude(m => m.MissionInstruments)
                          .ThenInclude(mi => mi.Instrument)
                   .Include(tr => tr.Mission)
                      .ThenInclude(m => m.MissionInstruments)
                          .ThenInclude(mi => mi.CognitiveEngineer)
                   .Include(tr => tr.ProcurementPart)
                      .ThenInclude(pp => pp.Part);
     }
 }

Then you can call it as you suggested:

var record = await db.DisplacementDamageRecords
         .TestRecordIncludes()
         .FirstOrDefaultAsync(tr => tr.Id == form.Id);

Edit:

In the comments you suggested that you have an inheritance where you have a a base class TestRecord and 2 subclasses: DisplacementDamageRecord and TestRecord. All the includes are based on the base class and you would like the extension method to work with the subclasses.

You can do this using a generic constraint. The updated version would be:

public static class DbExtensions
 {
     public static IQueryable<T> TestRecordIncludes<T>(this IQueryable<T> records) where T : TestRecord
     {
         return records
                  .Include(tr => tr.Mission)
                      .ThenInclude(m => m.MissionRadiationPartsLead)
                  .Include(tr => tr.Mission)
                      .ThenInclude(m => m.MissionInstruments)
                          .ThenInclude(mi => mi.Instrument)
                   .Include(tr => tr.Mission)
                      .ThenInclude(m => m.MissionInstruments)
                          .ThenInclude(mi => mi.CognitiveEngineer)
                   .Include(tr => tr.ProcurementPart)
                      .ThenInclude(pp => pp.Part);
     }
 }

I will suggest creating an extension method more generic and call it on your subclasses as needed or keep on your base class the navigation properties that your subclasses should have by default as an array of nav properties.

This extension method takes 2 parameters, 1 is a list of navigation properties to be included on the query and the other is includeAllRecords

    public virtual IQueryable<TEntity> GetAll(string[] includeNavProp = null, bool includeAllRecords = false)
    {
        IQueryable<TEntity> query = includeAllRecords ? TableNoTracking.IgnoreQueryFilters() : Table;

        if (includeNavProp != null)
        {
            query = includeNavProp.Aggregate(query, (current, includePath) => current.Include(includePath));
        }

        return query;
    }

includeAllRecords is used in case you have some query filters that exclude some rows from the final result. Some applications implement soft deletes, passing this parameter will include or exclude rows that were marked as deleted for example. See the idea below of a QueryFilter

public abstract class BaseEntityTypeConfiguration<T> : IEntityTypeConfiguration<T> where T : BaseEntity
{
    public virtual void Configure(EntityTypeBuilder<T> builder)
    {
        builder.HasKey(p => p.Id);
        builder.Property(dt => dt.Id).UseSqlServerIdentityColumn();

        builder.Property(x => x.CreatedBy).HasMaxLength(50).IsRequired();
        builder.Property(x => x.CreatedOn).IsRequired();
        builder.Property(x => x.UpdatedBy).HasMaxLength(50).IsRequired();
        builder.Property(x => x.UpdatedOn).IsRequired();

        builder.HasQueryFilter(app => !app.IsDeleted);
    }
}

With that said, you can call GetAll in this way

var ddr = db.DisplacementDamageRecords.GetAll(new []{ 
       "Mission.MissionRadiationPartsLead",
       "Mission.MissionInstruments.Instrument",
       "Mission.MissionInstruments.CognitiveEngineer",
       "Mission.ProcurementPart.Part")

Now it is up to you how you group your navigation properties to make them reusable.

Hope this helps

Related