Below is a first draft for a solution. I still have to find out if it's practicable ... and I'll consider to rework the loading approach (as Lanorkin suggested), too. Thank you for your comments.
Edit
It turned out that, while excludes might make sense when developing an application ...doing many changes to the domain model..., excludes are not more elegant than includes for a "real world example" that I just considered.
a) I went through my entities and counted the number of included and excluded navigation properties. The average number of excluded properties was not significantly smaller then the number of included properties.
b) If I do consider a distinct navigation property "foos" for the exclusions, I will be forced to consider exclusions for the sub entities of type Foo ... if I do not want to use its properties at all.
On the other hand, using inclusions, I just need to specify the navigation property "foos" and do not need to specify anything else for the sub entities.
Therefore, while excludes might save some specs for one level, they dent to require more specs for the next level ... (when excluding some intermediate entities and not only entities that are located at the leaves of the loaded object tree).
c) Furthermore, the includes/excludes might not only depend on the type of the entity but also on the path that is used to access it. Then an exclude needs to be specified like "exclude properties xy when loading the entity for one purpose and exclude properties z when loading the entity for another purpose".
=> As a result of this considerations I will go on using inclusions.
I implemented type save inclusions that are based on inclusion dictionaries instead of strings:
private static readonly Inclusions<Person> _personInclusionsWithCompanyParent = new Inclusions<Person>(typeof(Company))
{
{e => e.Company, false},
{e => e.Roles, true}
};
I have a method that creates the query from a list of inclusions. That method also checks if all existing navigation properties are considered in the dictionaries. If I add a new entity and forget to specify corresponding inclusions, an exception will be thrown.
Nevertheless, here is an experimental solution for using excludes instead of includes:
private const int MAX_EXPANSION_DEPTH = 10;
private DbContext Context { get; set; } //set during construction of my repository
public virtual IQueryable<TEntity> AllExcluding(string excludeProperties = "")
{
var propertiesToExclude = excludeProperties.Split(new[]
{
','
},
StringSplitOptions.RemoveEmptyEntries);
IQueryable<TEntity> initialQuery = Context.Set<TEntity>();
var elementType = initialQuery.ElementType;
var navigationPropertyPaths = new HashSet<string>();
var navigationPropertyNames = GetNavigationPropertyNames(elementType);
foreach (var propertyName in navigationPropertyNames)
{
if (!propertiesToExclude.Contains(propertyName))
{
ExtendNavigationPropertyPaths(navigationPropertyPaths, elementType, propertyName, propertyName, propertiesToExclude, 0);
}
}
return navigationPropertyPaths.Aggregate(initialQuery, (current, includeProperty) => current.Include(includeProperty));
}
private void ExtendNavigationPropertyPaths(ISet<string> navigationPropertyPaths,
Type parentType,
string propertyName,
string propertyPath,
ICollection<string> propertiesToExclude,
int expansionDepth)
{
if (expansionDepth > MAX_EXPANSION_DEPTH)
{
return;
}
var propertyInfo = parentType.GetProperty(propertyName);
var propertyType = propertyInfo.PropertyType;
var isEnumerable = typeof(IEnumerable).IsAssignableFrom(propertyType);
if (isEnumerable)
{
propertyType = propertyType.GenericTypeArguments[0];
}
var subNavigationPropertyNames = GetNavigationPropertyNames(propertyType);
var noSubNavigationPropertiesExist = !subNavigationPropertyNames.Any();
if (noSubNavigationPropertiesExist)
{
navigationPropertyPaths.Add(propertyPath);
return;
}
foreach (var subPropertyName in subNavigationPropertyNames)
{
if (propertiesToExclude.Contains(subPropertyName))
{
navigationPropertyPaths.Add(propertyPath);
continue;
}
var subPropertyPath = propertyPath + '.' + subPropertyName;
ExtendNavigationPropertyPaths(navigationPropertyPaths,
propertyType,
subPropertyName,
subPropertyPath,
propertiesToExclude,
expansionDepth + 1);
}
}
private ICollection<string> GetNavigationPropertyNames(Type elementType)
{
var objectContext = ((IObjectContextAdapter)Context).ObjectContext;
var entityContainer = objectContext.MetadataWorkspace.GetEntityContainer(objectContext.DefaultContainerName, DataSpace.CSpace);
var entitySet = entityContainer.EntitySets.FirstOrDefault(item => item.ElementType.Name.Equals(elementType.Name));
if (entitySet == null)
{
return new List<string>();
}
var entityType = entitySet.ElementType;
return entityType.NavigationProperties.Select(np => np.Name)
.ToList();
}
Include
you use in query. From my experience using more than two includes is a signal to rework loading approach, separate to several smaller queries etc – Johanson