Is there a way to access a private property in a where clause?

145 Views Asked by At

I'm trying to directly map my domain model to EF. To that end I introduced a private property in my model like so:

private ICollection<Tag> TagsInternal { get; set; }

public Article(Guid id, ... , IEnumerable<Tag> tags) : base(id)
{
   ...
   this.TagsInternal = new List<Tag>(tags.Where(i => i != null));
}

public IEnumerable<Tag> Tags { get { return this.TagsInternal.AsEnumerable(); } }

In order to get access for EF to the 'backing property' I added a handful of extension methods:

public static class FluentApiExtensions
{
   public static ManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType> 
      HasMany<TEntityType, TTargetEntityType>(this EntityTypeConfiguration<TEntityType> mapper,
      string propertyName)
      where TEntityType : class
      where TTargetEntityType : class
   {
      var lambda = GetLambdaExpression<TEntityType>(propertyName);

      return mapper
         .HasMany((Expression<Func<TEntityType, ICollection<TTargetEntityType>>>)lambda);
   }

   public static ManyToManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType> 
      WithMany<TEntityType, TTargetEntityType>(this ManyNavigationPropertyConfiguration<TEntityType, TTargetEntityType> mapper,
      string fieldName)
      where TEntityType : class
      where TTargetEntityType : class
   {
      var lambda = GetLambdaExpression<TTargetEntityType>(fieldName);

      return mapper
         .WithMany((Expression<Func<TTargetEntityType, ICollection<TEntityType>>>)lambda);
   }

   private static LambdaExpression GetLambdaExpression<T>(string propertyName)
   {
      var type = typeof (T);
      var parameterExpression = Expression.Parameter(type, "type");
      var expression = (Expression)parameterExpression;

      var propertyInfo = type
         .GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

      if (propertyInfo == null)
         throw new ArgumentException(string.Format("There is no property named '{0}' on type '{1}'.",
            propertyName, type.Name));

      var propertyExpression = Expression.Property(expression, propertyInfo);

      return Expression.Lambda(propertyExpression, parameterExpression);
   }
}

I am then in a position to pick up navigation properties like this:

public Maybe<Article> GetArticle(Guid articleId)
{
   articleId.MustNotBeNull();

   var article = this.unitOfWork.Context.Articles
      .Include("TagsInternal")
      .FirstOrDefault(a => a.Id == articleId);

   return article == null
      ? new Maybe<Article>()
      : new Maybe<Article>(article);
}

However, the whole thing breaks down when trying to execute a query that has "Tags" in its where clause, for example

var test = this.unitOfWork.Context.Articles
   .Where(a => a.Tags.Count() > 0);

The message is

The specified type member 'Tags' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

Is there any way to remedy the situation? I'm seriously considering to just keep EF out of the model and map instead or maybe go down the state object route as suggested by Vaughn Vernon.

1

There are 1 best solutions below

0
On

You said you're mapping your domain model to EF. So with that in mind you'll want to keep your EF classes as simple as possible. They only exist to persist your domain model. That means:

  • Do not put methods on EF classes.
  • Do not use anything other than the default constructor.
  • Only use public methods.
  • Map your domain classes to EF classes manually or use something like AutoMapper.

Also, based on your code above it looks like you have your own UnitOfWork implementation. This is unnecessary as DbContext is the unit of work. (Use your DI container to manage DbContext sessions. You probably want them to have a per-request lifetime if this is a web application.)

I'm also not sure why you're returning Maybe<Article> above. If it's not found, just return null. In your controller, if GetArticle() returns null, then return HttpNotFound() from the controller.

If do the above, that should clear up the confusion and remove a lot of the mapping code you have above.