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.
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:
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 manageDbContext
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, ifGetArticle()
returnsnull
, then returnHttpNotFound()
from the controller.If do the above, that should clear up the confusion and remove a lot of the mapping code you have above.