Introduction to Entity Framework 4.3 Migrations - Part II

In Part I I discussed the new Entity Framework Migrations and why they are so useful. In this part I want to actually walk you through the process of building a code-first project and using migrations to make changes to your database as you go.

1) For the sake of simplicity I'm going to start with an empty MVC 3 website. Once the project is created right-click the project and open up the NuGet package manager.

2) Once in the package manger, search for Entity Framework and click "Install". This will give you the latest Entity Framework DLL which as of this writing is 4.3.1.

3) Next we need to build our model. Add a few classes to the model folder. These are just going to be simple C# objects to define our model. That's the cool part about EF code-first is that your model does not even need to be aware that it is being used with Entity Framework.

public class Book  
{
    public int Id { get; set; }
    public string Title { get; set; }
    public DateTime PublishDate { get; set; }

    public virtual Genre Genre { get; set; }
    public virtual ICollection<Author> Authors { get; set; }
}

public class Author  
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Bio { get; set; }

    public virtual ICollection<Book> Books { get; set; }
}

public class Genre  
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Book> Books { get; set; }
}

Notice that some of the properties on our model are virtual. I don't need to get into too much detail but just know that this is a technique you can use when defining navigation properties that you want to be lazy loaded.

4) Currently there is no way for Entity Framework to know that it has to intervene. In order to complete our code-first model we need a context. For this example I will just place it in the root of the project.

public class LibraryContext : DbContext  
{
    public DbSet<Book> Books { get; set; }
    public DbSet<Genre> Genres { get; set; }
    public DbSet<Author> Authors { get; set; }
}

We give EF control by making our context inherit from DbContext. We then make it aware of what classes are part of our model by adding them as DbSet<T> properties on our context. Normally here we'd also override the OnModelCreating() method and use the Entity Framework fluent API to do some configuration, but for this example our model is so simple that we don't have to do that. EF can infer everything it needs to know from our current model. For example, including an integer property named "Id" follows a convention that allows EF to assume that property should be the primary key for that entity.

5) Believe it or not, at this point we're all done setting up Entity Framework code-first. Let's go write some controller logic to use our model and verify that it works. Add a new controller called HomeController and implement the Index action method.

public class HomeController : Controller  
{
    public ActionResult Index()
    {
        var context = new LibraryContext();
        return View(context.Books);
    }
}

6) Now implement the view to look like the following.

@model IEnumerable<EFMigrations.Models.Book>

@{
    ViewBag.Title = "Index";
}

<h2>All Books</h2>

@foreach (var book in Model)
{
    <div>
        Title: @book.Title<br />
        Publish Date: @book.PublishDate<br />
        Author: @foreach (var author in book.Authors)
                {
                    <span>@author.Name, </span>
                }<br />
        Genre: @book.Genre.Name
    </div>
}

@if (Model.Count() == 0)
{
    <div>
        There are no books in the database.
    </div>
}

7) If you run the project now it won't be very exciting. At least, not right away.

What you didn't see happen was when you ran the project it went ahead and built the database for you based on your code. Let's connect to our sqlexpress server to see for ourselves. You'll notice that you now have a database with the same name as your DbContext, EFMigrations.LibraryContext in this case. You'll also notice that this database contains tables that correspond with each of the entities we created.

8) Let's go ahead and add some data to our database. The easiest way to do this would be to add a button on our Index page that will trigger an action method that will add a series of fake books. Add this action method to your controller.

public ActionResult AddFakeData()  
{
    var context = new LibraryContext();
    var sciFi = new Genre
    {
        Name = "Science Fiction"
    };
    context.Books.Add(new Book
    {
        Title = "Time's Edge",
        PublishDate = new DateTime(2010, 10, 20),
        Genre = sciFi,
        Authors = new List<Author>
        {
            new Author
            {
                Name = "Joseph Dattilo",
                Bio = "J.M. Dattilo is the pen name for Joseph and Mary Dattilo, a husband and wife writing team. Time's Edge, the first book in the Time's Edge sci-fi/fantasy series, won a Tassy Walden Award, a literary prize given by the Shoreline Arts Alliance of Connecticut. Mary is also the author of the comedy \"Francine's Will\", a play that won first place in the Nutmeg Players New Works Festival. The couple lives in Connecticut. (Author photo by Kaitlin Dattilo.)"
            },
            new Author
            {
                Name = "Mary Dattilo",
                Bio = "J.M. Dattilo is the pen name for Joseph and Mary Dattilo, a husband and wife writing team. Time's Edge, the first book in the Time's Edge sci-fi/fantasy series, won a Tassy Walden Award, a literary prize given by the Shoreline Arts Alliance of Connecticut. Mary is also the author of the comedy \"Francine's Will\", a play that won first place in the Nutmeg Players New Works Festival. The couple lives in Connecticut. (Author photo by Kaitlin Dattilo.)"
            }
        }
    });
    context.Books.Add(new Book
    {
        Title = "Afterworld",
        PublishDate = new DateTime(2001, 4, 20),
        Genre = sciFi,
        Authors = new List<Author>
        {
            new Author
            {
                Name = "R. Vincent Riccio",
                Bio = @"R. Vincent Riccio was technically born in Italy on an Italian ocean liner, coming into New York, the son of naturalized US citizens. His education was in Catholic schools and college, through undergraduate degrees, and then some well-known colleges afterward for graduate work. He is educated in both English Literature, Biochemistry, and Psychology, later working in the scientific research fields of Psychology and Biochemistry. He began writing stories when he was 8 years old, and continued that passion throughout high school and college, eventually to the ABC-TV network for several years writing a wide variety of things from advertising and public relations to documentaries to original ideas for TV and radio. Later on he pursued more of his psychological background in consulting to personnel agencies and general general business, all the while maintaining his writing avocation. In the '90's he decided to publish his work, and came out with his now best-selling concept book, Afterworld in 2001, to which he recently added the long-awaited sequel, Afterworld 2: The Reformation. To this point, he has written 12 books, the good portion of them science fiction-fantasy, calling on his largely scientific background. He has written in virtually every venue, but audiences tend to have their own opinion on what they would like to see from you, and so he has put out more books in the sci-fi-fantasy area, and will likely continue to do so with brief imaginative excursions into other areas of interest."
            }
        }
    });
    context.SaveChanges();
    return RedirectToAction("Index");
}

We also need to add a link to this action somewhere in our view.

@Html.ActionLink("Add Fake Data", "AddFakeData")

9) Now run your app again and you should see a handy link.

After clicking the link you'll notice that your view now shows you some data. Ugly data yes, but we don't really care about that right now.

10) Okay, now let's go mess with our model. We're using code-first so we're just gonna go straight to our objects without even touching the database. Let's change the name of the Bio property on the Author entity to Biography. Easy right? You'd think so, but go ahead and run your application.

This used to be the part where you got to choose between two or three ways in which your existing data should be nuked. However, in 4.3 you now get a friendly error letting you know that you should use migrations to migrate your database schema.

11) Now for the not-so-intuitive part. Open up the NuGet Package Manager Console; there are three commands you will need to become familiar with in this console. The first one is "Enable-Migrations". You will only have to run this command once for your project, unless you delete the migrations files and need to re-enable migrations.

After running this command you'll notice several things. First you'll notice that you are now staring at a Configuration.cs file that inherits from DbMigrationsConfiguration<EFMigrations.LibraryContet>. Second, you'll notice that this file lives in a new "Migrations" folder added to the root of your project. In addition to this Configuration.cs file you'll see another file that looks something like 201203132150103_InitialCreate.cs. The last file is the file I mentioned in Part I that is added every time you do a migration. The "InitialCreate" one represents the current state of your database schema. Every time you do a migration a new file will be added here. The numbers in the first part of the file name are a time stamp ID so that they stay in the order that they were created for easy navigation.

12) The next two commands you'll need to learn are "Add-Migration" and "Update-Database". Add-Migration sets up a new migration cs file that records the change to the model/schema. Update-Database will execute any pending changes in the database. In the Configuration.cs file you should see a property called AutomaticMigrationsEnabled being set to false in the constructor. If you set this to true you will find that you are able to use Update-Database without doing Add-Migration most of the time. This will automatically execute the changes for you without creating a migration file. I personally don't like this method because you're trusting it to make all the proper changes for you and you're eliminating the nicety of being able to migrate your database back and forth. The ability to check-in those migration files to source control is so beneficial that I couldn't possibly see why you would omit that step.

Go ahead and type "Add-Migration" into the NuGet Package Manger Console to create the migration for our current change to our model. The utility will do its best to guess at how our changes should be applied in the database. After typing "Add-Migration" you will be prompted for a "Name". The name is the value that shows up after the time stamp ID in the migration file name. "InitialCreate" was the name of the first migration. This name is a little confusing because you don't want it to be long and it can't contain spaces since it's part of a file name. Try to name it something short but descriptive. For now let's just put "PropertyRename" down as the name.

You should now see your new migration file.

public partial class PropertyRename : DbMigration  
{
    public override void Up()
    {
        AddColumn("Authors", "Bioagraphy", c => c.String());
        DropColumn("Authors", "Bio");
    }

    public override void Down()
    {
        AddColumn("Authors", "Bio", c => c.String());
        DropColumn("Authors", "Bioagraphy");
    }
}

Pause for a second and look at what it's doing. We have two methods: one for migrating up to the next version of the database and one for migrating back down. Very useful for moving back and forth as needed. This all looks fine right? Wrong. Look what is about to happen in the Up() method. It is about to add a new column to the database and remove our old column. Well we already have data in that column so this change will surely nuke our data. Ouch! Lucky for us we added a migration file so we can tweak it before we actually execute it in the database. Modify it to look like this instead.

public partial class PropertyRename : DbMigration  
{
    public override void Up()
    {
        RenameColumn("Authors", "Bio", "Biography");
    }

    public override void Down()
    {
        RenameColumn("Authors", "Biography", "Bio");
    }
}

There was no way for the migration utility to know that we intended to simply rename a column unless we tell it. All it could see was that the entity had a property and now it doesn't and also it has a new property. If it had assumed that we wanted to rename a column it would have been correct in this case, but what if in the future you really did want to delete a column and you happened to add a new one at the same time? You'd then have the opposite problem.

13) Now that we've setup our migration how we want it, run the "Update-Database" command. This command will run all migrations that have not been run yet. In our case only the "PropertyRename" migration will be run. Once it's finished go ahead and run your application. You should see that nothing changed. Your data still displays. We aren't showing it in this project but if you open up your database you'll notice that the Biography column on the Authors table still contains it's data. This would not have been the case if we hadn't modified the migration.

Even with automatic migrations turned on "Update-Database" would not have worked because it would detect that there would be data loss it would force you to do "Add-Migration" first so it knows you are doing what you want, at that point it will be perfectly happy to lose data if you tell it to. However, I still think it's far more useful to create a migration file for every change to your model as it creates a nice little chronological story for your database that gets checked into source control. Do not underestimate the value of this. The ability to go back in time and see the database at various stages is priceless.

Chev

Read more posts by this author.

comments powered by Disqus