EF CTP4 Tips & Tricks: Find

This is the second in a series of posts taking a quick look at the some of the features in the recent Entity Framework Feature CTP4, also recently dubbed “EF Magic Unicorn Edition”. In the last post we looked at Include with lambda and today we are going to look at the new Find method.

The Problem

Let’s say I want to get a hold of the “FOOD” category. Prior to having the Find method I can do this pretty easily using LINQ:

var food = context.Categories.Single(c => c.CategoryId == "FOOD");

Actually the above line of code is going to query the database every time it runs so I should really use the TryGetObjectByKey method to avoid the query if “FOOD” is already loaded into memory. TryGetObjectByKey isn’t strongly typed so I need to supply an out parameter that is typed as object and then cast if it was found.

Category food;
object searchFood;
if (context.TryGetObjectByKey(key, out searchFood))
{
    food = (Category)searchFood;
}

Now let’s update the code to create the “FOOD” category if it doesn’t exist:

Category food;
object searchFood;
if (context.TryGetObjectByKey(key, out searchFood))
{
    food = (Category)searchFood;
}
else
{
    food = new Category { CategoryId = "FOOD", Name = "Food Products" };
    context.Categories.Add(food);
}

This works fine if I only run the code once but what if I want to find the “FOOD” category again? I now need to account for the fact that it might be sitting in the context in an added state. In this case it has a temporary key so TryGetObjectByKey won’t find it. That’s OK I have access to the ObjectStateManager to check for added objects:

Category food;
object searchFood;
if (context.TryGetObjectByKey(key, out searchFood))
{
    food = (Category)searchFood;
}
else
{
    food = context
        .ObjectStateManager
        .GetObjectStateEntries(System.Data.EntityState.Added)
        .Where(ose => ose.Entity != null)
        .Select(ose => ose.Entity)
        .OfType<Category>()
        .Where(c => c.CategoryId == "FOOD")
        .SingleOrDefault();

    if (food == null)
    {
        food = new Category { CategoryId = "FOOD", Name = "Food Products" };
        context.Categories.Add(food);
    }
}

The Solution

If you feel like you shouldn’t have to write that much code to achieve this fairly simple task you are definitely not alone. That block of code above can now be replaced with:

var food = context.Categories.Find("FOOD");
if (food == null)
{
    food = new Category { CategoryId = "FOOD", Name = "Food Products" };
    context.Categories.Add(food);
}

The rules for find are:

  1. Look for an entity with the supplied key that has already been loaded from the database
  2. If there isn’t one then check if there is an added entity that has the supplied key
  3. Finally query the database and if there still isn’t one that matches then return null

In CTP4 Find is an instance method on DbSet<T> so you do have to be using the new “Productivity Improvement” surface to get the benefit at least for the moment.

Composite Keys

Find takes a “params object[]” as it’s parameter so if you have composite keys you just specify the values for each key property:

var plate = context.LicensePlates.Find("WA", "555-555");

Code First requires that you specify the ordering of composite keys, you can do this via the Fluent API:

public class DMVContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<LicensePlate>().HasKey(p => new { p.State, Plate = p.Number });
    }

    public DbSet<LicensePlate> Plates { get; set; }
}

Or via attributes in you class:

public class LicensePlate
{
    [DataMember(Order = 0)]
    public string State { get; set; }

    [DataMember(Order = 1)]
    public string Number { get; set; }
}

“Yes” that is DataMember from the System.Runtime.Serialization namespace and “no” we shouldn’t make you add a reference to System.Runtime.Serialization.dll just to specify key ordering… we’ll fix that.

The Future

We’ve also looking at modifying the Add method so that it returns the newly added entity, this means we could reduce the code down to one line:

var food = 
    context.Categories.Find("FOOD") 
    ?? context.Categories.Add(new Category { CategoryId = "FOOD", Name = "Food Products" );

 

Ok ok that’s a bit long to actually put on one physical line… but you get the idea.

Summary

Finding objects based on primary key value(s) used to be a fairly painful exercise that required using advanced API surface… now it’s a lot simpler and encourages you to write performant code that reduces hits to the database.