Friday, February 15, 2013

EF Code First enum problem upgrading to EF5

What happens if your app uses enums in your POCO classes and you subsequently upgrade to EF5? How do you adapt now that EF supports enums?



Yesterday I upgraded my app to target .Net 4.5 (from 4.0) so I could install EF5 proper and promptly broke all the classes/tables that had enums in them. By the way, that app's not my app, as in "hands off it's mine", I just mean it's the app I'm working on, which should go without saying, but just so you don't think I'm a big boastful boaster I'm saying it again.



Well, the tables aren't broken, but they do have duplicate fields and values (red circled field in the screenshot above). I noticed this because I had a SQL script to populate them and started getting 'Cannot insert the value NULL into column 'AccommodationType', table 'PatientPortal.dbo.PatientDetails'; column does not allow nulls. INSERT fails.' errors where I obviously hadn't before: hadn't even had that column in that table before. What was going on? Here is the code for that class/table:
public class AdmissionDetails : IEntity
{
        public int Id { get; set; }

        … other properties … 

        public AccommodationType AccommodationType
        {
            get { return (AccommodationType)AccommodationTypeId; }
            set { AccommodationTypeId = (int)value; }
        }
        public int AccommodationTypeId { get; set; }
}
As you can see, we have a kind of superfluous member, in this case AccommodationTypeId, simply to map the underlying int value of the actual enum, AccommodationType. That's the standard way of faking enums that we all use. EF dutifully ignores any types it doesn't recognise, in this case the enum. That's why only the AccommodationTypeId column gets generated in the database. Once I upgraded to EF5 however, the web.config changes triggered a recreation of the database, and lo! EF saw the enum field, and generated a field for it, and it was good. Well, not quite. But it actually doesn't make a huge difference, since the new mapped enum field will simply do exactly the same as the int field. It will, though, result in a lot of spurious, unnecessary changes to your database, which you want like you want to listen to Coldplay.

It turned out the fix for all this is quite easy. One reason for that is that by default an enum resolves in the database to an int in EF5, so if you’re using ints like we were you can happily rip out all that plumbing code. What I did was:
  • got rid of the unnecessary int (AccommodationTypeId) representing the enum's underlying value.
  • removed the enum's (AccommodationType) getter and setter code, and made them auto-implemented properties
  • mapped the enum to the old id field in the database, with a data annotation ([Column("AccommodationTypeId")]) to map, avoiding a database schema change and potential data-loss, since without this the erstwhile Id column (AccommodationTypeId) would have had to be dropped
using System.ComponentModel.DataAnnotations.Schema; // Column annotation needs this

public class AdmissionDetails : IEntity
{
    public int Id { get; set; }

    ... other properties ...

    [Column("AccommodationTypeId")]
    public AccommodationType AccommodationType { get; set; }
}
The database schema reverts to the original state and since we don’t run EF Migrations in our team to upgrade our production database, rather relying on a Redgate schema difference tool, there's no difference between your local database schema and your production one! Noice.

Double trouble

Plain sailing so far. What could go wrong? An object composed with more than one instance of another object is what. Switching examples from accommodation types to people and addresses, a person object has a home address and a billing address, both of which might be represented by an Address class, making the Person object thus:
public class PersonDetails
{
    public int Id { get; set; }

    ... other properties ...

    public Address HomeAddress { get; set; }
    public Address MailingAddress { get; set; }
}
That's ok, but if the Address has an enum in there like, oh I don't know, State, like this...
public class Address
{
    public int Id { get; set; }

    ... other properties ...

    [Column("StateId")]
    public State State{ get; set; }
}
...then the problem you'll hit is you'd now have two StateIds in your Person table. 'Schema specified is not valid. Errors: (319,6) : error 0019: Each property name in a type must be unique. Property name 'StateId' was already defined'. That [Column("StateId")] annotation is a literal - it can't generate, as it should, separate columns called 'HomeAddress_StateId' and 'MailingAddress_StateId' the way EF would, left to its own devices. In this case you need to remove that [Column("StateId")] annotation, then switch to Data Annotations' big brother, the Fluent API.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity()
                .Property(p => p.HomeAddress.State)
                .HasColumnName("HomeAddress_StateId");

    modelBuilder.Entity()
                .Property(p => p.MailingAddress.State)
                .HasColumnName("MailingAddress_StateId");
}
Some might argue that all of your configuration should be done fluently like this, and they're probably right. I'd rather do what I can using the Data Annotations before resorting to the fluent API simply because the annotation metadata is close to the actual thing its annotating, and also, if you do everything in OnModelCreating that can become as messy as Bali traffic. But it's up to you.