Entity Framework: new() vs. DbSet.Create()

This is one of those "I had to explain this couple times already so next time, I want something I can redirect people to" kind of post.

What I want to write about is the difference in behavior between using new() and DbSet.Create() for instantiating new entities. In order to do this, I've created a very simple model and context:

public class Planet {
    public virtual int Id {
        get;
        set;
    }
    public virtual string Name {
        get;
        set;
    }
    ...
    public virtual ICollection Natives {
        get;
        set;
    }
}

public class Character {
    public virtual int Id {
        get;
        set;
    }
    public virtual string Name {
        get;
        set;
    }
    ...
    public virtual int HomeworldId {
        get;
        set;
    }
    public virtual Planet Homeworld {
        get;
        set;
    }
}

public interface IStarWarsContext {
    DbSet Planets {
        get;
        set;
    }
    DbSet Characters {
        get;
        set;
    }
    int SaveChanges();
}

public class StarWarsContext: DbContext, IStarWarsContext {
    public DbSet Planets {
        get;
        set;
    }
    public DbSet Characters {
        get;
        set;
    }
}


I've also created a very simple view that lists characters already present in the database and allows for adding new ones.

@using (Html.BeginForm())
{
    <fieldset>
        <legend>New StarWars Character</legend>
        <div>
            @Html.LabelFor(m => m.Name)
        </div>
        <div>
            @Html.TextBoxFor(m => m.Name)
        </div>
        <div>
            @Html.LabelFor(m => m.Homeworld)
        </div>
        <div>
            @Html.DropDownListFor(m => m.Homeworld, Model.Planets)
        </div>
        ...
        <div>
            <input type="submit" value="Add" />
        </div>
    </fieldset>
}
<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Homeworld</th>
            ...
        </tr>
    </thead>
    <tbody>
        @foreach (Character character in Model.Characters)
        {
            <tr>
                <td>@character.Name</td>
                <td>@character.Homeworld.Name</td>
                ...
            </tr>
        }
    </tbody>
</table>


The view is powered by the following ViewModel and controller.

public class StarWarsViewModel {
    public string Name {
        get;
        set;
    }

    ...

    public int Homeworld {
        get;
        set;
    }

    public IEnumerable Planets {
        get;
        set;
    }

    public IReadOnlyList Characters {
        get;
        set;
    }
}

public class StarWarsController: Controller {
    private IStarWarsContext _startWarsContext;

    public StarWarsController(IStarWarsContext startWarsContext) {
        _startWarsContext = startWarsContext;
    }

    [HttpGet]
    public ActionResult Index() {
        return View(GetViewModel());
    }

    [HttpPost]
    public ActionResult Index(StarWarsViewModel viewModel) {
        AddCharacter(viewModel);

        return View(GetViewModel());
    }

    private StarWarsViewModel GetViewModel() {
        return new StarWarsViewModel {
            Planets = _startWarsContext.Planets
                .Select(p => new {
                    p.Id, p.Name
                })
                .ToList()
                .Select(p => new SelectListItem {
                    Value = p.Id.ToString(), Text = p.Name
                }),
                Characters = _startWarsContext.Characters.ToList()
        };
    }

    private void AddCharacter(StarWarsViewModel viewModel) {
        throw new NotImplementedException();
    }
}


The AddCharacter method is the point of interest here. There are two ways to implement it, and they will result in a different behavior.

Creating Entities With new()

Following the first Entity Framework tutorial that pops up on Google will result in code similar to this:

private void AddCharacter(StarWarsViewModel viewModel) {
    Character character = new Character();
    character.Name = viewModel.Name;
    ...
    character.HomeworldId = viewModel.Homeworld;

    _startWarsContext.Characters.Add(character);
    _startWarsContext.SaveChanges();
}


Running this code and adding a new Character will result in a NullReferenceException coming from the part of view that generates the table (to be more exact, it comes from @character.Homeworld.Name). The reason for the exception is the fact that Entity Framework needs to lazy load the Planet entity, but the just-added Character entity is not a dynamic proxy, so lazy loading doesn't work for it. Only Entity Framework can create a dynamic proxy, but in this scenario, there is no way for it to do that — the caller already owns the reference to the entity, and it cannot be changed to a different class.

Creating Entity With DbSet.Create()

In order to be able to create new entities as proper dynamic proxies, the DbSet class provides the Create method. This method returns a new dynamic proxy instance that isn't added or attached to the context. To use it, only a single line of code needs to be changed.

private void AddCharacter(StarWarsViewModel viewModel) {
    Character character = _startWarsContext.Characters.Create();
    character.Name = viewModel.Name;
    ...
    character.HomeworldId = viewModel.Homeworld;

    _startWarsContext.Characters.Add(character);
    _startWarsContext.SaveChanges();
}


After this simple change, the code works as expected — related entities are being lazy loaded when needed.

The Takeaway

The sample above is built in a way that highlights the difference between new() and DbSet.Create() (it even has an N+1 selects hiding in there for the sake of simplicity). In real life, this rarely causes an issue, as there are a couple of other things that can impact the behavior (related entity already present in context or usage of the Include() method, for example). But when this causes an issue, it's always unexpected, and I've seen smart people struggling to wrap their heads around what is going on. It is important to understand the difference and use both mechanisms appropriately (sometimes lack of lazy loading may be desired).

 

 

 

 

Top