1

I have a service that passes in parameters for how much I want to include for navigation properties. Based upon the boolean args it concatenates an entity list to include each required foreign entity.

At runtime I want to include either no navigation entities or many. What I can't do is daisy chain with .Include().Include as I don't know which and how many to include based around passed in args.

I want to achieve this, but I don't seem to be able to pass in a comma separated entity list. Any ideas?

var res = db.Entity.Include(entityListCommaSeparated).Where(_=>_.ID == ID).FirstOrDefault();
pfx
  • 20,323
  • 43
  • 37
  • 57
SeanK
  • 15
  • 1
  • 3
  • As Steve Py mentions below, I would suggest using the lambda expressions rather than magic strings for includes. If you ever have to refactor you will get compile time errors rather than more obscure runtime database errors (or worse, no errors and no includes being returned). – Wurd Sep 04 '18 at 09:50
  • I couldn't use the lambda expressions in this instance as i needed to change the includes at runtime based upon boolean include parameters. ie 15 bools with defaults of false which may include any combination of the corresponding navigation properties. With lambda expressions that's 2^15 (32768) separate lambdas to support all possible combinations of includes. ie GetPolicy(int id, bool includeEntityA = false, bool includeEntityB = false, .... – SeanK Sep 04 '18 at 18:42

1 Answers1

1

This looks like a repository pattern, and generally gets messy if you want to try and "hide" EF / the DbContext from calling code.

A couple options you can consider:

  1. Down the complexity rabit hole: use a params Expression<Func<TEntity, object>>[] includes in your applicable repository methods, and then be prepared to also pass OrderBy expressions, as well as pagination values when you want to return multiple entities.
  2. THrough the simplicity mirror: Embrace IQueryable as a return type and let the consumers handle Includes, OrderBy's, Counts/Any/Skip/Take/First/ToList, and .Select() as they need.

Option 1:

public Order GetById(int id, params Expression<Func<Order, object>>[] includes)
{
     var query = db.Orders.Where(x => x.ID == id);
     // This part can be moved into an extension method or a base repository method.
     if(includes.Any)  
        includes.Aggregate(query, (current, include) => 
        {
            current.Include(include);
        }
     // Don't use .FirstOrDefault() If you intend for 1 record to be returned, use .Single(). If it really is optional to find, .SingleOrDefault()
     return query.Single();
}
//ToDo
public IEnumerable<Order> GetOrders(/* criteria?, includes?, order by?, (ascending/descending) pagination? */)
{ }
// or
public IEnumerable<Order> GetOrdersByCustomer(/* includes?, order by?, (ascending/descending) pagination? */)
{ }
// plus..
public IEnumerable<Order> GetOrdersByDate(/* includes?, order by?, (ascending/descending) pagination? */)
{ }
public bool CustomerHasOrders(int customerId)
{ }
public bool OrderExists(int id)
{ }
public int OrdersOnDate(DateTime date)
{ }
// etc. etc. etc.

Keep in mind this doesn't handle custom order by clauses, and the same will be needed for methods that are returning lists of entities. Your repository is also going to need to expose methods for .Any() (DoesExist) because everyone loves checking for #null on every return. :) Also .Count().

Option 2:

public IQueryable<Order> GetById(int id)
{
    return db.Orders.Where(x => x.ID == id);
}
public IQueryable<Order> GetOrders()
{
    return db.Orders.AsQueryable();
}

Callers can grok Linq and .Include() what they want before calling .Single(), or do a .Any().. They may not need the entire entity graph so they can .Select() from the entity and related entities without .Include() to compose and execute a more efficient query to populate a ViewModel / DTO. GetById might be used in a number of places so we can reduce duplication and support it in the repository. We don't need all of the filter scenarios etc, callers can call GetOrders and then filter as they see fit.

Why bother with a repository if it just returns DBSets?

  1. Centralize low-level data filtering. For instance if you use Soft Deletes (IsActive) or are running multi-tenant, or explicit authorization. These common rules can be centralized at the repository level rather than having to remembered everywhere a DbSet is touched.
  2. Testing is simpler. While you can mock a DbContext, or point it at an in-memory database, mocking a repository returning IQueryable is simpler. (Just populate a List<TEntity> and return .AsQueryable().
  3. Repositories handle Create and Delete. Create to serve as a factory to ensure that all required data and relationships are established for a viable entity. Delete to handle soft-delete scenarios, cascades/audits etc. beyond what the DB handles behind the scenes.
Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • I've tried the below which is similar to your answer, but i'm missing something. // This hard coded line works policy = db.Policys.Include("EntityA").Include("EntityB").Where(_ => _.ID == ID).FirstOrDefault(); // This code doesn't work IQueryable policies = db.Policys.Where(_ => _.ID == ID); foreach (string entity in include.Split(',') { policies.Include(entity); } policy = policies.FirstOrDefault(); The above works on the top level object parent object, but not the foreign entities. They return null. – SeanK Sep 03 '18 at 23:35
  • `foreach (string entity in include.Split(',') { policies = policies.Include(entity); } ` You were missing the assignment back to policies. – Steve Py Sep 04 '18 at 00:34