Missing EF Feature Workarounds: Encapsulated collections

The list of missing EF features is quite long, but several of the items in the list do have workarounds. For encapsulated domain models that enforce their own consistency boundary, encapsulating a collection is quite important to ensure your domain model stays consistent. We might have an operation that needs to invoke some side effects as the result of adding or removing from the collection:

public class Order {
    public List<OrderLineItem> LineItems { get; set; }
    public decimal Total { get; private set; }
    public void AddLineItem(Product product, int amount) {
        var lineItem = new OrderLineItem(product, amount);
        LineItems.Add(lineItem);
        Total += lineItem.ItemTotal;
    }
}

But since we expose the collection directly, we can run into some inconsistencies:

var order = new Order();
order.LineItems.Add(new OrderLineItem(product, 10));
Debug.Assert(order.Total == 0);

To address this issue and only expose operations which we want to allow, we invoke the encapsulate collection refactoring:

public class Order {
    private readonly List<OrderLineItem> _lineItems;

    public Order() {
        _lineItems = new List<OrderLineItem>();
    }

    public IEnumerable<OrderLineItem> LineItems { get { return _lineItems; } }
    public decimal Total { get; private set; }
    public void AddLineItem(Product product, int amount) {
        var lineItem = new OrderLineItem(product, amount);
        _lineItems.Add(lineItem);
        Total += lineItem.ItemTotal;
    }
}

Our collection is now fully encapsulated, however, EF doesn’t support mapping to private fields. You can do tricks to trick EF to map to a field, but underneath the covers, it won’t be able to do things like tracked entities, lazy loading or eager fetching.

So we’re stuck using a property. But what if we used a protected property instead? Or exposed the expression in other ways? One way is to use expression tree rewriting to refer to the public readonly property in one way, but trick EF into looking at a protected property instead (thanks hazzik). First, let’s add our protected backing property instead of that private field (protected to get lazy loading etc):

public class Order {
 
    public Order() {
        _lineItems = new List<OrderLineItem>();
    }
 
    protected List<OrderLineItem> _lineItems { get; set; }
    public IEnumerable<OrderLineItem> LineItems { get { return _lineItems; } }
    public decimal Total { get; private set; }
    public void AddLineItem(Product product, int amount) {
        var lineItem = new OrderLineItem(product, amount);
        _lineItems.Add(lineItem);
        Total += lineItem.ItemTotal;
    }
}

We’ve encapsulated our private field into a protected property with the same naming scheme, so only my Order class (or the proxy subclass) can access the full collection. None of my other code needed to change. Next, we’ll use the DelegateDecompiler project to help rewrite our expressions that refer to the IEnumerable property. First, our DelegateDecompiler configuration:

public class EntityFrameworkMappingConfiguration : DefaultConfiguration
{
    private readonly HashSet<MemberInfo> members = new HashSet<MemberInfo>();
 
    public void RegisterForDecompilation(MemberInfo member)
    {
        members.Add(member);
    }
    public override bool ShouldDecompile(MemberInfo member)
    {
        return members.Contains(member) || base.ShouldDecompile(member);
    }
}

Then we’ll need to expose our own custom HasMany EF Code First extension method that redirects the property from our IEnumerable one to the backing field. DelegateDecompiler peers into the property of my IEnumerable, recognizes that it’s referring to another property, and then we swap out our IEnumerable property with the found ICollection one:

public static class EntityTypeConfigurationEx
{
    private static readonly EntityFrameworkMappingConfiguration cfg = new EntityFrameworkMappingConfiguration();

    static EntityTypeConfigurationEx()
    {
        Configuration.Configure(cfg);
    }

    public static ManyNavigationPropertyConfiguration<TEntityType, TTargetEntity> HasMany<TEntityType, TTargetEntity>(
        this EntityTypeConfiguration<TEntityType> c,
        Expression<Func<TEntityType, IEnumerable<TTargetEntity>>> navigationPropertyExpression)
        where TTargetEntity : class where TEntityType : class
    {
        var body = navigationPropertyExpression.Body;
        var member = (PropertyInfo) ((MemberExpression) body).Member;
        cfg.RegisterForDecompilation(member);
        var decompile = DecompileExpressionVisitor.Decompile(body);
        var convert = Expression.Convert(decompile, typeof (ICollection<TTargetEntity>));
        var expression = Expression.Lambda<Func<TEntityType, ICollection<TTargetEntity>>>(convert, navigationPropertyExpression.Parameters);
        return c.HasMany(expression);
    }
}
 

When we configure EF Code First, our custom HasMany method will be called instead of the EF Code First one, which only accepts ICollection. From inside an EntityTypeConfiguration class:

public class OrderMapping :
    EntityTypeConfiguration<Order> {
    public OrderMapping() {
        this.HasMany(m => m.LineItems);
    }
}

EF will now use the protected property to load behind the scenes, even though we’re referring to the public property. There is a catch, however, in that if we filter based on that property or use Include, we’ll need to run through the same expression redirection process. Since my EF queries flow through AutoMapper projections, I made sure that I call the DelegateDecompiler’s Decompile method in my extensions:

public static List<TDestination> ToList<TDestination>(
    this IProjectionExpression projectionExpression)
{
    return projectionExpression.To<TDestination>().Decompile().ToList();
}

public static IQueryable<TDestination> ToQueryable<TDestination>(
    this IProjectionExpression projectionExpression)
{
    return projectionExpression.To<TDestination>().Decompile();
}

public static TDestination[] ToArray<TDestination>(
    this IProjectionExpression projectionExpression)
{
    return projectionExpression.To<TDestination>().Decompile().ToArray();
}

public static TDestination ToSingleOrDefault<TDestination>(
    this IProjectionExpression projectionExpression)
{
    return projectionExpression.To<TDestination>().Decompile().ToList().SingleOrDefault();
}

And finally, I need to create my own Include method that replaces EF:

public static IQueryable<T> Include<T, TEnumerable>(this IQueryable<T> queryable,
    Expression<Func<T, IEnumerable<TEnumerable>>> path)
{
    var newPath = (Expression<Func<T, IEnumerable<TEnumerable>>>)
        DecompileExpressionVisitor.Decompile(path);
    return QueryableExtensions.Include(queryable, newPath);
}

Now in my code, I’ve injected my expression replacing into the mapping configuration and query pipeline so that if I refer to the collection property, the expression gets redirected to the backing protected property. If I had naming conventions, I could just use reflection to load the property myself, but with the DelegateDecompiler method, it will use whatever the public IEnumerable property accesses.

This certainly isn’t the only way to encapsulate collections in EF, but it does allow me to have minimal impact to the surface API of my domain model.  While EF doesn’t support encapsulated collections out of the box, with some extensions and redirection, I can continue on with an encapsulated domain model.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jimmy Bogard

I'm a technical architect with Headspring in Austin, TX. I focus on DDD, distributed systems, and any other acronym-centric design/architecture/methodology. I created AutoMapper and am a co-author of the ASP.NET MVC in Action books.
This entry was posted in Entity Framework. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • jarrettmeyer

    Or, just use NHibernate which has this ability natively and requires no EF silliness.

    • jbogard

      I wrote a little while back why I switched for one particular project – just domain modeling alone isn’t enough to pick one or the other.

    • Bob

      Or use something that isn’t an incredibly pain in the arse to get set up.

    • http://blog.waynemolina.com Wayne Molina

      Certainly valid, but what if you work for a company that only uses EF? Besides the obvious “I wouldn’t work for a company like that” answer, if you have to use EF might as well make the best out of it to use it properly.

    • santa

      Sorry but can’t resist the troll…
      Relational Databases and ORMs … LOL

      • jbogard

        Not a very effective troll…

        • santa

          NHibernate is soooo 2008! … and EF meh!
          Point is… ORM “wars” in 2014 sounds silly … use the one you feel more comfortable with, if you have a choice.
          Still good tips about EF.

  • cidico

    Are you trying to prove a point with those posts?
    I mean…
    Are you wanting to be able to say “Hey world! Entity Framework is ‘usable’!”

    • gleb Chermennov

      seems Jimmy says that EF is usable for a particular project he works on

  • João P. Bragança

    Does this work with private properties too?

    • jbogard

      No idea.

  • gleb Chermennov

    Ok, I don’t know about you guys, but if I want to make common scenario like this one to work and I use EF, adding another library, no matter the size, and a mount of code just to make it work is not a pleasant thing, especially if I’m new to EF

    • Mike Cole

      As of the last time I worked with NHibernate, you needed NHibernate.Linq for Linq support that’s baked right into EF (and EF’s is better). You also needed Fluent NHibernate to have any sort of rational mapping functionality. You needed LinFu or Castle for lazy loading.

      Is that still the case?

      • gleb Chermennov

        Only Fluent NHibernate is still needed, Linq and lazy loading capabilities are baked in

        • Mike Cole

          Is Lazy Loading using a native NHibernate provider, or is it still using LinFu/Castle with the libraries merged?

          • http://alexeyzimarev.blogspot.com Alexey Zimarev

            LinFu is still used. It is one of the targets for NH 4, which doesn’t get released for years.

          • http://weblogs.asp.net/ricardoperes Ricardo Peres

            NH no longer needs it. While you still can specify what proxy generator to use (LinFu, Spring, Castle), it has it’s own.

      • jbogard

        All of that is built in now, no need for extra libraries. Fluent NH is pretty much dead, you can use the ConfORM mappings instead.

        • Mike Cole

          Thanks for the info.

        • gleb Chermennov

          not so sure about Fluent being dead, people are using it, and it’s MUCH better than NHibernate mapping by code thanks to conventions

          • http://alexeyzimarev.blogspot.com Alexey Zimarev

            It is not “better”. With NH mapping by code you know what sort of mapping the framework will generate. If you use FluentNH you are completely blind unless you dig into the code of FluentNH to see what kind of mapping it does for different situations.

          • http://weblogs.asp.net/ricardoperes Ricardo Peres

            Agreed. James Gregory once wrote a famous post saying he was giving up on FNH, but I understand there have been progresses since. The problem about FNH, IMHO, is that its API is not complete, and the names don’t exactly match NHibernate’s, so whoever is used to .HBM.XML mappings will be a bit lost there. But, yes, it’s far more friendly than NH’s own fluent mapping API.

          • Bob

            If you use nHibernate, you are completely blind to how it will generate SQL unless you log all the queries.. If you compile code, you are completely blind to how it will behave unless you dig into the generated bytecode.. etc. Granted, FNH doesn’t translate as literally as it should, and accessing the generated mappings is just a PITA, but the convention stuff is priceless, and keeps code a lot dry-er than having to repeat mappings. Last I checked there didn’t seem to be that same convention support in the baked in NH mappings. Unless this has changed, I won’t be switching.

          • jbogard

            You should check again, these conventions do exist (albeit a different form) in the conform mappings.

      • http://alexeyzimarev.blogspot.com Alexey Zimarev

        NH has mapping by code since 3.2. It might look a bit cumbersome but for those who know a little bit about xbm mappings it is a very good option. We moved everything from FlientNH to mapping by code.

        I am not sure what “rational mapping” means but in EF earlier versions everything was in one huge XML file.

        Linq support in NH is quite good.

        • Mike Cole

          Bad choice of wording on my part – I meant reasonable mapping functionality, meaning the XML files were hard to work with.

          Thanks for the info. NHibernate is/was a great product to work with.

          • http://weblogs.asp.net/ricardoperes Ricardo Peres

            Hi, Mike!
            As said, NH has fluent mappings which also includes conventions (with hbm.xml and attributes, that’s more than EF has), so there’s no need for FNH.
            LINQ is built-in into the NHibernate.dll for quite some time.

  • Pingback: Missing EF Feature Workarounds: Encapsulated collections | Dot Net RSS

  • Sandor Drieënhuizen

    Nice article, works pretty good. Too bad it breaks migration scaffolding though.

    • jbogard

      How so? I don’t use EF migrations.

      • Sandor Drieënhuizen

        I tried to reproduce the error but I can’t seem to get it to crash when enabling/adding migrations anymore (I went back in the project history to the point where I was experimenting with your code in it). I remember the error was something along the lines of ‘could not evaluate expression’ and the expression contained several type casts.

  • nazardo

    Hi. But if you hide collections, how can you write a query like this one:
    (from o in context.Orders
    where o.LineItems.Any()
    select o)

    Wouldn’t it raise a “can’t translate in SQL” error?

    Thanks,

    N.