Testing EF4 Code First Optimistic Concurrency with Fluent Assertions

Entity Framework 4 Code First CTP4 was released in July and the API for developing without any edmx continues to mature and get better. Code First allows you to define your entity framework configuration with a Fluent API similar to Fluent NHibernate. Although the API for EF Code First is very user friendly, you should still have integration tests to verify that you've configured your entities correctly. This post will walk through configuring EF4 Code First optimistic concurrency.

Suppose I have a POCO Task class that looks like this:

public class Task
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime? DueDate { get; set; }
    public int Status { get; set; }
    public string Notes { get; set; }
    public byte[] Timestamp { get; set; }
}

Notice on line #8 I have a byte array that I've named Timestamp. I'm going to map this to a SQL Server timestamp column. First I'll create my DbContext. DbContext is a new type in EF Code Only that simplifies ObjectContext to streamline on the most frequently used methods.

class TasksDbContext : DbContext
{
    public TasksDbContext(DbModel dbModel) : base(dbModel)
    {
    }
 
    public DbSet<Task> Tasks { get; set; }
}

Notice the constructor of my database context takes a DbModel. There are a couple of different ways I can create this. The first is to configure everything using the model builder like this:

var modelBuilder = new ModelBuilder();
modelBuilder.Entity<Task>().Property(m => m.Timestamp).IsConcurrencyToken().HasStoreType("timestamp").IsComputed();
var model = modelBuilder.CreateModel();
var context = new TasksDbContext(model);

The key is line #2 above where I fluently configure the timestamp property of my Tasks class to be 1) identified as a concurrency token, 2) specified as a SQL Server timestamp data type, and 3) a computed column so that EF knows that the user will not be inserting values into this field but rather, the value will be computed on the SQL Server itself. Although it's syntactically convenient to be able to do all this inline with the ModelBuilder, as your apps get bigger, that can get ugly. In this case, it's better to separate your configuration for each entity in EntityConfiguration classes like this:

class TaskConfiguration : EntityConfiguration<Task>
{
    public TaskConfiguration()
    {
        this.Property(m => m.Timestamp).IsConcurrencyToken().HasStoreType("timestamp").IsComputed();
    }
}

I've only added one line of configuration code here but, when I need to specify additional mapping/configuration details about the Task entity, I'll do it here.  Now I can simply reference that entity configuration with the model builder. Hence, I'll create a simple context factory class:

class ContextFactory
{
    private static DbModel dbModel = InitializeModel();
 
    private static DbModel InitializeModel()
    {
        var modelBuilder = new ModelBuilder();
        modelBuilder.Configurations.Add(new TaskConfiguration());
        return modelBuilder.CreateModel();
    }
 
    public static TasksDbContext CreateContext()
    {
        return new TasksDbContext(dbModel);
    }
}

Now anytime I need a database context, I can just call my factory method. Before we get to integration testing our data access code, let's wrap up our EF code in a simple repository so our consumers don't need any knowledge of EF.

public class TaskRepository : IRepository<Task>
{
    public void Add(Task item)
    {
        using (var context = ContextFactory.CreateContext())
        {
            context.Tasks.Add(item);
            context.SaveChanges();
        }
    }
 
    public void Update(Task item)
    {
        using (var context = ContextFactory.CreateContext())
        {
            context.Tasks.Attach(item);
            context.ChangeObjectState(item, System.Data.EntityState.Modified);
            context.SaveChanges();
        }
    }
 
    public Task FindById(int id)
    {
        using (var context = ContextFactory.CreateContext())
        {
            return context.Tasks.FirstOrDefault(x => x.Id == id);
        }
    }
    // Remove() and FindAll() methods omitted for brevity
}

A couple things of note: first, I'm simply using my ContextFactory to create the context anytime I need it. Second, notice line #17 above calls a method on the database context called ChangeObjectState(). I added this method to TasksDBContext in order to get at the ObjectStateManager in the ObjectContext. This allows me to mark an entity as modified (i.e., a UPDATE should occur) that came in from another tier and was created in another context somewhere. The entire method looks like this:

public void ChangeObjectState<T>(T entity, EntityState entityState)
{
    this.ObjectContext.ObjectStateManager.ChangeObjectState(entity, entityState);
}

In EF4 DbContext, the ObjectContext is a protected property. Hence, if you want to get at it, you have to do so in the context itself (i.e., I could not have referred to it from my repository directly).

At this point, we're ready to execute our integration tests. To do this, I'm going to use MSTest with Fluent Assertions. Fluent Assertions is a CodePlex project that enables you to use a fluent-style API to more natural specify your expected outcome. The API for MSTest has been basically stagnant for the last several years and this adds functionality onto it so you can use MSTest and still get the benefit of fluent assertions. Additionally, Fluent Assertions enables us to get away from the clumsy [ExpectedException] attribute to a more deterministic xUnit-style ShouldThrow() API.

Our unit test will have two different repositories (and therefore 2 different contexts) get the same task. Then the first repository will save. When the second repository saves, we'll expect an OptimisticConcurrencyException:

[TestMethod]
public void Save_with_wrong_timestamp_should_fail()
{
    // arrange
    var taskId = 1;
 
    var repository1 = new TaskRepository();
    var task1 = repository1.FindById(taskId);
 
    var repository2 = new TaskRepository();
    var task2 = repository2.FindById(taskId);
 
    task1.Notes = "modified note";
    repository1.Update(task1);
    task1.Notes.Should().Be("modified note");
 
    task2.Notes = "modified note 2";
 
    // act
    Action action = () => repository2.Update(task2);
 
    // assert
    action.ShouldThrow<OptimisticConcurrencyException>();
}

Notice line #15 above – it shows a simple example of the Should().Be() syntax that Fluent Assertions gives you for MSTest. Next look at line #20 and #23. Here is see we are deterministically asserting that that specific line of code will throw the expected exception. This is much better than the [ExpectedException] attribute as that could give false positives given that any line of code in your unit test could potentially cause an exception.

As a final step, let's check out what happens behind the scenes when this test runs by using the Entity Framework Profiler. This tool is not free but it's simply the best tool I've seen for profiling EF where people typically resort to using the SQL Profiler. If you're doing any serious EF work, it's definitely worth the money. To use the EF Prof, just add the required assembly per the documentation (HibernatingRhinos.Profiler.Appender.v4.0.dll) and then add the 1-line code to the beginning of your method: EntityFrameworkProfiler.Initialize();

efprofoptcon

There are a couple of interesting things to notice in our EF Prof output. First, the WHERE clause on line #6-7 above shows what we'd expect with Optimistic Concurrency – that is, it looks at both the Id and the concurrency token column.  The second interesting thing is that EF prof is showing a red alert bubble. When we click on this we see, "Transaction disposed without explicit rollback/commit". So EF Prof not only shows you the complete profiling results but also suggests ways to improve your code.

Certainly data access code is nothing new. However, with EF4 Code First, integration testing with Fluent Assertions, and debugging with EF Prof, I must say, it's more fun than it's been in a while. I highly recommend checking all these out.

Tweet Post Share Update Email RSS