gio
gio

Reputation: 31

WebAPI and EntityFramework *without* LazyLoading

In my current project, I am using WebAPI and EntityFramework model-first. After some investigation, it appears that I have to use LazyLoading in order to load related entities.

I have read in many blogs that using LazyLoading in a service can cause performance and serialization problems - so I'd like to avoid it if possible.

Any suggestions on how I can achieve this without creating POCO objects?

Upvotes: 3

Views: 449

Answers (1)

Troy Alford
Troy Alford

Reputation: 27256

The simple answer to your question is - use .Include().

As an example: imagine that you have a Customer object which has an associated ReferredBy object, which references another customer. In your application, you want to return a list of Customers as well as the associated Customer who referred each one.

Your WebAPI method might look something like:

[HttpGet]
public IQueryable<Customer> Customers() {
    return db.Customers.OrderBy(c => c.LastName).Top(10);
}

When the serializer gets ahold of this, you are liable to run into various errors, which you can read more about in this article. In essence, though, it comes from 2 things:

  1. ProxyCreation / LazyLoading - which is EF-speak for "load the associated objects for my object graph on-demand, only when I ask for them", and
  2. Serialization of cycles - which means - A refers to B, and B refers to A - so every time I serialize one, I serialize the other again as a child of it. This creates an infinite loop.

I won't get into all of the specifics or the additional problems - I already gave you an article which goes into it in some depth. Instead, here is the way I solve the issue in my applications:

  1. Use JSON.net as your serializer. You can refer to this link for instructions on how to set it up in Visual Studio as your project's default serializer (assuming it isn't already)
  2. In your Global.asax or one of the .cs files which is loaded as part of your configuration, use the following settings:

    config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = 
        Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    

    This tells JSON.net not to serialize reference loops.

  3. Set the MergeOption in your DataContext and each of its Collection properties to MergeOption.NoTracking. In my applications, I do this by editing the .tt file which creates the DataContext:

    First, find the line that creates the constructor, and change to:

    MergeOption _defaultMergeOption = MergeOption.AppendOnly;
    
    public <#=code.Escape(container)#>() : this("name=<#=container.Name#>") { }
    public <#=code.Escape(container)#>(String connectionString) : base(connectionString) {
    <# if (!loader.IsLazyLoadingEnabled(container)) { #>
        this.Configuration.LazyLoadingEnabled = false;
    <# } #>
        this.Configuration.ProxyCreationEnabled = false;
        _defaultMergeOption = MergeOption.NoTracking;
    }
    

    Find the line that starts with:

    <#  foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>()) { #>
    

    Now edit the next few lines to say:

    <#  foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>()) { #>
    <#= Accessibility.ForReadOnlyProperty(entitySet)#> ObjectQuery<<#=typeMapper.GetTypeName(entitySet.ElementType)#>> <#=code.Escape(entitySet)#> {
        get {
            var set = (((IObjectContextAdapter)this).ObjectContext).CreateObjectSet<<#=typeMapper.GetTypeName(entitySet.ElementType)#>>();
            set.MergeOption = _defaultMergeOption;
    
            return set;
        }
    }
    <#  }
    

    What this does, is give you a constructor for your DataContext which can default all of the collections to MergeOption.NoTracking, and automatically disable ProxyCreation and LazyLoading. When you create an instance of your DataContext to pull from, you can now simply say:

    var db = new MyDataContext("ConnectionStringGoesHere");
    

Given the above, your new WebAPI method becomes as simple as:

[HttpGet]
public IQueryable<Customer> Customers() {
    return db.Customers.Include("ReferredBy")
             .OrderBy(c => c.LastName).Top(10);
}

The .Include() will load the child-record as part of the initial SQL statement (one hit to the database in total), and the Serializer will ignore the back-references, allowing you to produce JSON similar to:

[{
    Id: 1,
    FirstName: 'Frank',
    LastName: 'Abba',
    ReferredBy: {
        Id: 4,
        FirstName: 'Bob',
        LastName: 'Jones',
        ReferredBy: null
    }
 }, {
    Id: 4,
    FirstName: 'Bob',
    LastName: 'Jones',
    ReferredBy: null
 }
}]

Upvotes: 1

Related Questions