I have been developing the skeleton of a database that will support versioning of data using Code First in EF 4.3.1.
I had the models persisting and loading properly a couple of days ago but I have broken something since and I can’t figure out what is wrong. All of the classes get mapped and tables are created, data is persisted as well. So in the storring direction, everything works fine! BUT, when I try to load a Registration entity, the values are all what the default constructor is setting them to. I’m thinking maybe the data isn’t being loaded after the Registration constructor is called, but I’m at the end of my current abilities to figure out what’s happening!
The fundamentals are these two classes, from which my verion-able classes derive…
public abstract class VersionBase<T> {
[Key]
public Int64 Id { get; protected set; }
public DateTime CreationDateTime { get; protected set; }
// Value is virtual to support overriding to let deriving classes specify attributes for the property, such as [Required] to specify a non-nullable System.String
public virtual T Value { get; internal set; }
protected VersionBase() {
CreationDateTime = DateTime.Now;
}
protected VersionBase(T value)
: this() {
Value = value;
}
}
public abstract class VersionedBase<TVersion, TBase>
where TVersion : VersionBase<TBase>, new() {
[Key]
public Int64 Id { get; protected set; }
public virtual ICollection<TVersion> Versions { get; protected set; }
protected VersionedBase() {
Versions = new List<TVersion>();
}
[NotMapped]
public Boolean HasValue {
get {
return Versions.Any();
}
}
[NotMapped]
public TBase Value {
get {
if (HasValue)
return Versions.OrderByDescending(x => x.CreationDateTime).First().Value;
throw new InvalidOperationException(this.GetType().Name + " has no value");
}
set {
Versions.Add(new TVersion { Value = value });
}
}
}
Examples of derived classes…
public class VersionedInt32 : VersionedBase<VersionedInt32Version, Int32> { }
public class VersionedInt32Version : VersionBase<Int32> {
public VersionedInt32Version() : base() { }
public VersionedInt32Version(Int32 value) : base(value) { }
public static implicit operator VersionedInt32Version(Int32 value) {
return new VersionedInt32Version { Value = value };
}
}
…and…
public class VersionedString : VersionedBase<VersionedStringVersion, String> { }
public class VersionedStringVersion : VersionBase<String> {
public VersionedStringVersion() : base() { }
public VersionedStringVersion(String value) : base(value) { }
public static implicit operator VersionedStringVersion(String value) {
return new VersionedStringVersion { Value = value };
}
/// <summary>
/// The [Required] attribute tells Entity Framework that we want this column to be non-nullable
/// </summary>
[Required]
public override String Value { get; internal set; }
}
My calling code is as such…
static void Main(String[] args) {
using (var db = new VersionedFieldsContext()) {
Registration registration = new Registration();
registration.FirstName.Value = "Test";
registration.FirstName.Versions.Add("Derp");
db.Registration.Add(registration);
db.SaveChanges();
}
using (var db = new VersionedFieldsContext()) {
Registration registration = db.Registration.First();
// InvalidOperationException at next line: "VersionedString has no value"
String asdf = registration.FirstName.Value;
}
}
public class Registration {
[Key]
public Int64 Id { get; set; }
public DateTime CreationDateTime { get; set; }
public VersionedString FirstName { get; set; }
public Registration() {
CreationDateTime = DateTime.Now;
FirstName = new VersionedString();
}
}
public class VersionedFieldsContext : DbContext {
public DbSet<Registration> Registration { get; set; }
public VersionedFieldsContext() {
Database.SetInitializer<VersionedFieldsContext>(new DropCreateDatabaseIfModelChanges<VersionedFieldsContext>());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
Thanks for any insight!
You need two changes:
Remove instantiation of
FirstNamefrom theRegistrationconstructor so that the constructor is only:Creating an instance of a navigation reference (not collections) in an entity’s default constructor causes known problems: What would cause the Entity Framework to save an unloaded (but lazy loadable) reference over existing data?
If you have fixed the first point, your custom exception changes to a
NullReferenceException. To fix this make theFirstNameproperty inRegistrationvirtualbecause the code in your secondusingblock requires lazy loading:Edit
A workaround to create a registration and automatically instantiate the
FirstNamemight be a factory method:EF uses the default constructor when it materializes a
Registrationobject. In your custom code you could use the factory method when you need to create an instance ofRegistration:It can be less useful though when you work with change tracking or lazy loading proxies and want to create a proxy instance manually:
This again will call the default constructor and you have to instantiate the
FirstNameafter the object is already created.