2
\$\begingroup\$

The aim of this class is to set rules for including navigational properties on Configuration level and then apply where needed in the code.

I've decided to store expressions in the list for different entities and then invoke System.Data.Entity.QueryableExtensions Include method to get proper JOINs for my navigational properties. It does work, but at what price?

How optimal and efficient is the following solution:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Data.Entity;
using XXX.Data.Security;
using XXX.Extensions;

namespace XXX.Data.Entity.ModelConfiguration {

internal class IncludedReferencePropertiesConfiguration<TEntity> where TEntity : class {

    #region Constructors

    private IncludedReferencePropertiesConfiguration(LoanAdminContext dbContext) {

        this.DbContext = dbContext;
    }

    #endregion Constructors

    #region Properties

    #region Instance

    private static IncludedReferencePropertiesConfiguration<TEntity> instance;

    public static IncludedReferencePropertiesConfiguration<TEntity> Instance(LoanAdminContext dbContext) {

        //Always refresh DbContext

        if (instance == null) {
            instance = new IncludedReferencePropertiesConfiguration<TEntity>(dbContext);
        } else {
            instance.DbContext = dbContext;
        }

        return instance;
    }

    #endregion Instance

    #region Includes

    private static readonly IList<Expression<Func<TEntity, Object>>> Includes = new List<Expression<Func<TEntity, object>>>();

    #endregion Includes

    #region DbContext

    private LoanAdminContext DbContext { get; set; }

    #endregion DbContext

    #region Include<Expression<Func<TEntity, Object>>>

    /// <summary>
    /// Expression list which should keep the included referenced navigation properies
    /// </summary>
    public static void Include(Expression<Func<TEntity, Object>> path) {
        if (Includes.All(include => include.PropertyName() != path.PropertyName())) {
            Includes.Add(path);
        }
    }

    #endregion Include<Expression<Func<TEntity, Object>>>

    #region ApplyIncludes<Expression<IQueryable<TEntity>>

    /// <summary>
    /// Applies includes to entities
    /// </summary>
    public IQueryable<TEntity> ApplyIncludes(IQueryable<TEntity> entities) {

        foreach (var include in Includes) {
            var includeType = include.Property().PropertyType;
            var includeMethod = typeof(System.Data.Entity.QueryableExtensions).GetMethods()
                .Single(method =>
                    method.Name == "Include"
                    &&
                    method.IsGenericMethod
                    &&
                    method.GetParameters().Count() == 2
                    &&
                    method.GetParameters()[0].ParameterType.IsGenericType
                    &&
                    method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
                    &&
                    method.GetParameters()[1].ParameterType.IsGenericType
                    &&
                    method.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>));

            var includeGenericMethod = includeMethod.MakeGenericMethod(typeof(TEntity), includeType);

            entities = (IQueryable<TEntity>)includeGenericMethod.Invoke(null, new object[] { entities, Cast(includeType, include) });
        }

        return entities;
    }

    #endregion ApplyIncludes<IQueryable<TEntity>>

    #endregion Properties

    #region Public Functions

    #region LoadIncludedProperties

    public void ApplyIncludes(TEntity entity) {

        // Loop through all include path expressions and load corresponding properties
        foreach (var navigationPropertyExpression in Includes) {
            var navigationPropertyName = navigationPropertyExpression.PropertyName();
            var navigationPropertyInfo = entity.GetType().GetProperty(navigationPropertyName);
            if (navigationPropertyInfo != null) {
                var navigationPropertyEntities = navigationPropertyInfo.IsCollectionType()
                    ?
                    this.DbContext.Entry(entity).Collection(navigationPropertyName).Query()
                    :
                    this.DbContext.Entry(entity).Reference(navigationPropertyName).Query();
                navigationPropertyEntities = this.DbContext.CreateAllActionableObjectsQuery(navigationPropertyInfo.UnderlyingType(), navigationPropertyEntities, ActionType.Read);
                navigationPropertyEntities.Load();
            }
        }
    }

    #endregion LoadIncludedProperties

    #endregion Public Functions

    #region Private Functions

    private static Expression Cast(Type toType, Expression<Func<TEntity, Object>> expression) {

        var converted = Expression.Convert(expression.Body, toType);

        return Expression.Lambda(converted, expression.Parameters);
    }

    #endregion Private Functions
}
}

namespace XXX.Data.Entity.ModelConfiguration {

internal static class EntityTypeConfigurationExtensions {

    public static EntityTypeConfiguration<TEntity> Include<TEntity>(this EntityTypeConfiguration<TEntity> configuration, Expression<Func<TEntity, Object>> path)
        where TEntity : class {

        // Add eagerly loaded reference property to configureation class
        IncludedReferencePropertiesConfiguration<TEntity>.Include(path);

        // Return reference to configuration, for chaining of calls
        return configuration;
    }
}
}

Could I have made it more elegant and shorter?

\$\endgroup\$
2
  • \$\begingroup\$ "How optimal and efficient is the following solution" that depends. In your experience, how does it perform? Is it fast enough for your needs? ;-) \$\endgroup\$ – Dan Jul 24 '15 at 8:31
  • \$\begingroup\$ I still did not check it on serious amounts of data... :( So now it's all theoretical ;) \$\endgroup\$ – Alexander Jul 24 '15 at 9:28
1
\$\begingroup\$

Is it performant? I don't see any real problem since you're basically calling Include() via reflection, unless I'm missing something.

At what cost? Maintainability. In isolation, this method isn't bad. How you are planning on using it is the kicker. It raises the following questions:

  • Will this lead to a bunch of ad hoc bootstrapping code at app start?
  • How is this different than simply calling Include()?
  • Are you looking to eliminate boilerplate Includes by taking advantage of some meta-prerequisite, like the fact that a bunch of your entities have the same properties? If so, are you prepared to deal with the pain of that changing? Why not do the same thing and also make this known at compile time by doing something like partialling out your entities and applying a common interface, and wiring up your includes based on that interface?
\$\endgroup\$
5
  • \$\begingroup\$ Thanks for attention to my question. Hmm, regarding interfaces... I honestly cannot imagine how to apply them in my case. I have tons of EntityTypeConfiguration<TEntity> classes. Each of them describe some, let's say, part of admin interface. There's still a security part, which I don't want to talk about here not to over-complicate the question... But let's say so: by calling static Include method from corresponding configuration class I set the rules of includes for TEntity. And in generic TEntity methods, which are applicable to instances, I apply these rules by using ApplyIncludes. \$\endgroup\$ – Alexander Jul 25 '15 at 23:04
  • \$\begingroup\$ @Alexander I would recommend implementing these rules in an object or layer that evaluates the security context and rules at query time, and calls include based on those rules. I know it may seem more difficult to do it that way, but it actually allows for simplicity. Consider: a) your rules are already known at compile-time so any changes to the rules require a recompilation anyway b) if you wanted to unit test this you would only be able to verify that the expression is evaluated, and you would have to create painful integration tests to check that specific rules are being followed. \$\endgroup\$ – moarboilerplate Aug 25 '15 at 15:19
  • \$\begingroup\$ @Alexander I guess I don't really understand the need to conditionally call include based on security rules anyway. What's the problem with including the items in a non-admin context? Just performance concerns? \$\endgroup\$ – moarboilerplate Aug 25 '15 at 15:39
  • \$\begingroup\$ Well... Maybe I'm wrong because of lacking experience, but it seems to be that in given case if something gonna break - it's mainly because of EF bugs. Because what I do is just delegating including functionality to EF extensions method. Of course, applying includes "as is" is more flexible, but in our case we know for sure, that for a given entity a way of loading / non-loading navigation properties is the same across whole application - so we just "trigger" this action with ApplyIncludes and know it would produce the same result everywhere. \$\endgroup\$ – Alexander Aug 26 '15 at 14:06
  • \$\begingroup\$ Sorry for confusing you with "security rules": there is no direct relationship to current solution. I should not have mentioned this in current question maybe... :) \$\endgroup\$ – Alexander Aug 26 '15 at 14:08
0
\$\begingroup\$

The ApplyIncludes() method can be imrpoved by extracting the conditions of the lambda expression of the Single() method to a separate method.

In this extracted method we have the ability to store calculated values in some variables and therefor avoid the need to call for instance the method.GetParameters() method as the worst case 5 times and we can do this in a readable and maintainable manner.

Let us call this extracted method IsIncludeMethod(MethodInfo) and implement this call into the former code

public IQueryable<TEntity> ApplyIncludes(IQueryable<TEntity> entities) {

    foreach (var include in Includes) {
        var includeType = include.Property().PropertyType;
        var includeMethod = typeof(System.Data.Entity.QueryableExtensions).GetMethods()
            .Single(method => IsIncludeMethod(method));

        var includeGenericMethod = includeMethod.MakeGenericMethod(typeof(TEntity), includeType);

        entities = (IQueryable<TEntity>)includeGenericMethod.Invoke(null, new object[] { entities, Cast(includeType, include) });
    }

    return entities;
}  

which now looks much cleaner.

By changing the order of how the conditions will be evaluated and storing "calculated" values this `IsIncludeMethod() could look like so

private bool IsIncludeMethod(MethodInfo method)
{

    if (method.Name != "Include") { return false; }
    if (!method.IsGenericMethod) { return false; }

    var parameters = method.GetParameters();

    return (parameters.Length == 2
            &&
            parameters[0].ParameterType.IsGenericType
            &&
            parameters[1].ParameterType.IsGenericType
            &&
            parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
            &&
            parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>));
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks a lot for useful receipt ;) Cannot upvote, not enough reputation... \$\endgroup\$ – Alexander Aug 26 '15 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.