What I ended up doing was combining the Repository Pattern with Pablo's answer. The gist of it is that my EF models are versioned, I use EF Code-First Migrations to migrate the database to the new versions of the models, my DbContext always works with the latest version of the models, I developed a number of concrete repositories that each implement the IRepository<TItem> interface below.
public interface IRepository<TItem> : IQueryable<TItem>, ICollection<TItem>, IDisposable
where TItem : class
{
void Update(TItem item);
void SaveChanges();
}
One implementation of IRepository<TItem> is DbRepository<TItem> which wraps the entity framework code used to talk to the database.
public class DbRepository<TItem> : IRepository<TItem>
where TItem : class
{
private MyDbContext _db;
public DbRepository()
{
_db = new MyDbContext();
}
// Implementation of IRepository<TItem> methods
}
Another implementation of IRepository<TItem> is TypeConversionRepository<TExternal,TInternal> which is an abstract class that facilitates converting from one model type to another.
public abstract class TypeConversionRepository<TExternal, TInternal> : IRepository<TExternal>
where TExternal : class
where TInternal : class
{
protected IRepository<TInternal> InternalRepository { get; set; }
protected abstract TInternal ConvertInbound(TExternal externalItem);
protected abstract TExternal ConvertOutbound(TInternal internalItem);
// Implementation of IRepository<TItem> methods
}
Methods that return models or accept models as parameters use ConvertInbound() and ConvertOutbound() to convert models of type TExternal to TInternal and vice versa. Therefore, given the following 2 versions of MyModel, we can write 2 versions of MyModelRepository; version 2 can talk directly to the database while version 1 will need to convert from version 2 back to version 1.
namespace Models.v1
{
public class MyModel
{
public int Id { get; set; }
public string MyProperty { get; set; }
}
public class MyModelRepository : TypeConversionRepository<Models.v1.MyModel,Models.v2.MyModel>
{
MyModelRepository()
{
this.InternalRepository = new Models.v2.MyModelRepository();
}
protected override TInternal ConvertInbound(TExternal externalItem)
{
return new Models.v2.MyModel
{
Id = externalItem.Id,
MyNewProperty = externalItem.MyProperty
};
}
protected override TExternal ConvertOutbound(TInternal internalItem)
{
return new Models.v1.MyModel
{
Id = internalItem.Id,
MyProperty = internalItem.MyNewProperty
};
}
}
}
namespace Models.v2
{
public class MyModel
{
public int Id { get; set; }
public string MyNewProperty { get; set; }
}
public class MyModelRepository : DbRepository<MyModel>
{
}
}
Now the v1 ApiController can use the v1 MyModelRepository, the v2 ApiController can use the v2 MyModelRepository, but in the end all requests utilize a database that has been migrated to v2.