Freigeben über


My Favorite Features: Entity Framework Code First and ASP.NET Web API

It’s an exciting time for Entity Framework! Last Thursday, the team announced an open source release, which is now available on the Entity Framework CodePlex Site. I’ve been using Entity Framework quite a bit in my personal development, and thought I’d use this opportunity to continue my “Favorite Features” series with a post on EF.

In this post, I’m going to use the Code First workflow in Entity Framework to build an application. Entity Framework originally shipped as part of the .NET Framework, but in an effort to ship more often the last few versions have been shipped on NuGet, in-between releases of the .NET Framework. In addition to being available on NuGet, the latest version of Entity Framework (EF5) is also included in a number of places in Visual Studio 2012.

As part of building the app, I’ll also use ASP.NET Web API to build an HTTP service. ASP.NET Web API is another favorite feature of mine, and is great way to create services that can be consumed from different clients.

What’s New in EF5

I thought I’d start with a quick recap of the new features in Entity Framework 5, some of which we’ll get to use in this post. I’ve been looking forward to a number of these, and am happy to see them added in the latest release.

New features in Entity Framework 5:

· Enum support is a long awaited feature for EF and allows you to have enum properties in your domain classes. EF5 makes enum support available in the EF Designer and Code First.

· Table-valued functions in an existing database can now be included in models that are created using the EF Designer.

· Spatial data types can now be exposed in your model using the DbGeography and DbGeometry types. Spatial data can be included in models created with the EF Designer or Code First.

· EF5 also includes some significant performance improvements which you can find out more about on the ADO.NET Blog.

The EF Designer, which is included in Visual Studio 2012, also has some new features:

· Models can now be split up into multiple diagrams, which is great for working with larger models. You can also apply coloring to entities to help identify subsections of your model.

· Improvements to the wizard that reverse engineers a model from a database make it easier and quicker to batch import stored procedures for querying data.

Creating the Application

One of the places that EF5 shows up in Visual Studio 2012 is the project template for ASP.NET Web API, so if you create a new ASP.NET Web API project you have Entity Framework 5 available in your project by default.

In this post, I’ll create a simple web application to help folks find interesting tourist attractions. I’ll create a web page to help them locate the tourist attraction closest to a given location. Rather than having the web page communicate directly with the database, I’ll create a Web API to expose the data, so that applications on a variety of platforms can also access the data.

I’ll start by creating a new MVC 4 application, targeting .NET 4.5, and select the ‘Web API’ template.

NewProject

Building a Model

One of the great things about Code First is that building your model is as simple as defining a set of classes. If needed, you can also supply additional configuration to define what the database looks like and how your classes map to the database.

In the Models folder of my project I’ll add a Models.cs file that contains the following classes.

using System.Collections.Generic;

using System.Data.Spatial;

 

namespace TouristAttractions.Models

{

    public class TouristAttraction

    {

        public int TouristAttractionId { get; set; }

        public string Name { get; set; }

        public DbGeography Location { get; set; }

 

        public List<Review> Reviews { get; set; }

    }

 

    public class Review

    {

        public int ReviewId { get; set; }

        public string Author { get; set; }

        public string Comments { get; set; }

 

        public int TouristAttractionId { get; set; }

        public TouristAttraction TouristAttraction { get; set; }

    }

}

Adding an API Controller

Now that I have a model, I’m going to create an API controller that allows me to read and write tourist attractions. An API controller is similar to an MVC controller, except it is consumed as a service rather than hooked up to a set of views.

I need to first build the application before I add a controller. Next I’ll right-click on Controllers folder and select ‘Add->Controller…’ from the context menu. I’ll name the new controller ‘AttractionsController’, and select the ‘API controller with read/write actions, using Entity Framework’ template. This template will generate code that uses Entity Framework to read and write instances of my TouristAttraction class.

I can also specify which Entity Framework context I want to use for data access. A context is a class that represents a session with the database and allows me to read and write data. I haven’t created a context yet, so I’ll select <New data context…> from the list, and a new context will be created for me (I’ll call it TourismContext).

AddController

When I complete the Add Controller Wizard, a number of items get added to my project. The new TourismContext that I asked the wizard to create has been added to the Models folder. This class derives from DbContext, which is the primary API of Entity Framework. The context also exposes a DbSet for each type that I want to access. DbContext has a set of conventions to pick a database to try and use, but ASP.NET Web API has overridden these conventions and told my context to load its connection information from the config file (using the “name=” syntax in the constructor).

public class TourismContext : DbContext

{

    public TourismContext() : base("name=TourismContext")

    {

    }

 

    public DbSet<TouristAttraction> TouristAttractions { get; set; }

}

Sure enough, if I look in Web.config I find the TourismContext connection string, pointing my context to a LocalDb database called TourismContext (followed by a timestamp to ensure it doesn’t collide with any previous ASP.NET Web API projects).

<connectionStrings>

    <add name="TourismContext"
connectionString="Data Source=(localdb)\v11.0; Initial Catalog=TourismContext-20120618160809; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|TourismContext-20120618160809.mdf"
providerName="System.Data.SqlClient" />

  </connectionStrings>

Finally, the API controller itself has been added to the Controllers folder. The controller is coded up to support getting all TouristAttractions, finding a TouristAttraction with a certain key, and adding, updating and deleting TouristAttractions. I’ll just show part of the generated code for the sake of brevity:

public class AttractionsController : ApiController

{

    private TourismContext db = new TourismContext();

 

    // GET api/Attractions

    public IEnumerable<TouristAttraction> GetTouristAttractions()

    {

        return db.TouristAttractions.AsEnumerable();

    }

 

    // GET api/Attractions/5

    public TouristAttraction GetTouristAttraction(int id)

    {

        TouristAttraction touristattraction = db.TouristAttractions.Find(id);

        if (touristattraction == null)

        {

            throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));

        }

 

        return touristattraction;

    }

...

}

I’ll also add a new GetTouristAttraction method to AttractionsController, that takes some coordinates and finds the closest attraction.

public TouristAttraction GetTouristAttraction(double longitude, double latitude)

{

    var location = DbGeography.FromText(
string.Format("POINT ({0} {1})", longitude, latitude));

 

    var query = from a in db.TouristAttractions

                orderby a.Location.Distance(location)

                select a;

 

    return query.FirstOrDefault();

}

Consuming the Web API

Because ASP.NET Web API uses the basics of HTTP for communication, my service can be consumed from any number of platforms and technologies. As just one example, I’ll build a simple web page that accesses the service from JavaScript.

I’ll replace the contents of Views\Home\Index.cshtml with the following code:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

    "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html lang="en">

<head>

    <title>Find Nearest Attraction</title>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <script charset="UTF-8" type="text/javascript"

        src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.3&mkt=en-us">

    </script>

    <script src="../../Scripts/jquery-1.6.2.min.js" type="text/javascript"></script>

    <script type="text/javascript">

        function search() {

            var lat = $('#latitude').val();

            var long = $('#longitude').val();

 

            $.ajax({

                url: "api/Attractions/?longitude=" + long + "&latitude=" + lat,

                type: "GET",

                success: function (data) {

                    if (data == null) {

                        $('#attractionName').html("No attractions to search");

                    }

                    else {

                        $('#attractionName').html("You should visit " + data.Name);

                        displayMap(data.Location.Geography.WellKnownText, data.Name);

                    }

                }

            });

   }

 

        function displayMap(coordinateString, name) {

            // WellKnownText is in format 'POINT (<longitude>, <latitude>)'

            coordinateString = coordinateString.replace("POINT (", "").replace(")", "");

            var long = coordinateString.substring(0, coordinateString.indexOf(" "));

            var lat = coordinateString.substring(coordinateString.indexOf(" ") + 1);

 

            // Show map centered on nearest attraction

            var map = new VEMap('myMap');

            map.LoadMap(new VELatLong(lat, long), 15, VEMapStyle.Aerial);

 

            // Add a pin for the attraction

            var pin = new VEShape(VEShapeType.Pushpin, new VELatLong(lat, long));

            pin.SetTitle(name);

            map.AddShape(pin);

        }

    </script>

</head>

<body>

    <h1>Find the Closest Tourist Attraction</h1>

    <div>

        <label for="longitude">Longitude:</label>

        <input type="text" id="longitude" size="10" />

        <label for="latitude">Latitude:</label>

        <input type="text" id="latitude" size="10" />

        <input type="button" value="Search" onclick="search();" />

    </div>

    <p id="attractionName"></p>

    <div id='myMap' style="position: absolute; width: 400px; height: 400px;"></div>

</body>

</html>

The main functionality of this page is in the search function. When the user specifies the coordinates to search for, this function uses an HTTP PUT request to ask the service for the closest attraction. The displayMap function then uses Bing Maps to display the data visually.

If I run the project, I can test that the page appears correctly. However, we don’t have any data to display yet:

WebPageNoData

Now you may be wondering how it connected to the database when I didn’t do anything to create one. Because my database didn’t exist, Entity Framework Code First used a set of conventions to determine what the schema should look like, and created it for me. If I look at my LocalDb instance, I see the new database that it created:

Database

Rest assured, if you don’t like the default conventions there are plenty of options for changing table names, column names, data types and almost every other aspect of the schema that gets created. Code First can also be used to map to an existing database. That is beyond the scope of this post but you can check out the EF Power Tools as a good starting point.

Seeding the Data

Now I want to get some seed data into my database so that we can see the web page working properly. I’m going to use Entity Framework Code First Migrations to do this. The Migrations feature allows me to evolve the database as my model changes (we’ll see this in action shortly), and it also allows me to specify some seed data that should be present in the database.

To start using Code First Migrations, I can use the Enable-Migrations command in the Package Manager Console to switch on Migrations for my TourismContext. Package Manager Console can be opened from the Tools->Library Package Manager menu in Visual Studio.

EnableMigrations

Enabling migrations has added a new Migrations folder to my project that contains two items:

· Configuration.cs – This file allows me to define settings for how my database is to be migrated. This includes things such as the folder that migrations are generated in, seed data to be applied to the database and registration of providers for third party databases (i.e. MySql).

· <timestamp>_InitialCreate.cs – This is a migration that represents the changes that I’ve already applied to the database (i.e. creation of the TouristAttractions and Reviews tables). The migrations feature has recorded in my local database that this migration has already been applied.

I’ll modify Configuration.cs to specify some seed data in the Seed method. You’ll notice that I’m using the AddOrUpdate method which allows me to specify a property to match on – in my case that is the Name of the TouristAttraction. If Migrations finds an existing TouristAttraction with that name, it will update its current values to match the ones I’ve supplied; if not it will insert a new TouristAttraction.

protected override void Seed(TouristAttractions.Models.TourismContext context)

{

    context.TouristAttractions.AddOrUpdate(a => a.Name,

        new TouristAttraction

        {

            Name = "Space Needle, Seattle",

            Location = DbGeography.FromText("POINT(-122.348670959473 47.619930267334)")

        });

 

    context.TouristAttractions.AddOrUpdate(a => a.Name,

        new TouristAttraction

        {

            Name = "Pike Place Market, Seattle",

            Location = DbGeography.FromText("POINT(-122.341697692871 47.6094245910645)")

        });

 

    context.TouristAttractions.AddOrUpdate(a => a.Name,

        new TouristAttraction

      {

            Name = "Statue of Liberty, NY",

            Location = DbGeography.FromText("POINT(-74.0439682006836 40.6886405944824)")

        });

}

Now I can run the Update-Database command from Package Manager Console and the seed data will be applied to my local database. When I run the application, I can see this in action:

WebPage

Changing the Model

I have a simple application up and running, but it’s likely that my requirements will change over time. For example, I may decide I want to let users provide a Rating when they review an attraction.

public class Review

{

    public int ReviewId { get; set; }

    public string Author { get; set; }

    public string Comments { get; set; }

    public int Rating { get; set; }

 

    public int TouristAttractionId { get; set; }

    public TouristAttraction TouristAttraction { get; set; }

}

However when I update my model, it no longer matches the database schema that Code First created. If I were to run the application I would get an InvalidOperationException stating that “The model backing the 'TourismContext' context has changed since the database was created. Consider using Code First Migrations to update the database”.

I’ve already enabled Migrations to work with seed data though, so I can run the ‘Add-Migration AddRating’ command to scaffold my pending changes to a new migration, and give it a name (‘AddRating’).

Running this command adds a new <timestamp>_AddRating.cs file to the Migrations folder. This file contains the steps to make my database schema match the current model:

namespace TouristAttractions.Migrations

{

    using System;

    using System.Data.Entity.Migrations;

   

    public partial class AddRating : DbMigration

    {

        public override void Up()

        {

            AddColumn("dbo.Reviews", "Rating", c => c.Int(nullable: false));

        }

       

        public override void Down()

        {

            DropColumn("dbo.Reviews", "Rating");

        }

    }

}

Because this is just scaffolded code, I could edit it. For example, I could create indexes, or add columns or tables that aren’t part of my Code First model. I could also use the Sql method to execute any arbitrary SQL as part of the migration.

For this example the scaffolded code is fine, so I’ll run the Update-Database command in Package Manager Console. This command will check that database to see which migrations have been applied, compare that to the migrations in my project and apply any pending ones. Now that the schema is updated, I can run my application again and everything works just fine.

Conclusion

I hope this post gave you some good insight into Entity Framework Code First and ASP.NET Web API. To learn about Entity Framework, I recommend visiting the Entity Framework Developer Center and the Entity Framework Team Blog. For Web API, please visit https://www.asp.net/web-api.

Enjoy!

 

Follow me at https://twitter.com/jlzander .

Comments

  • Anonymous
    July 24, 2012
    Thanks for sharing.  The Web API example was simple and straightforward.

  • Anonymous
    July 24, 2012
    Any chance, any of this will be available in VS2010?

  • Anonymous
    July 24, 2012
    @roteague - You can use EF5 with VS2010 but you will only get bug fixes. The new features listed in this post require .NET 4.5, which in turn requires VS2012.

  • Anonymous
    July 24, 2012
    @roteague - Web API is available for VS2010, see this page for more details http://www.asp.net/web-api

  • Anonymous
    August 09, 2012
    Regarding the DbGeography type, it is not currently working with the WCF Ria domain service classes. Is there a way to get this to work?

  • Anonymous
    August 10, 2012
    @lymber - Unfortunately, no; there are no plans to update RIA Services to add support for the DbGeography type. Because RIA Services needs to understand how to serialize/deserialize types to/from strings to pass over URLs and post-data, its list of supported types is implemented as an "allow list" of known types.  Therefore new types that get introduced will not work with RIA Services.

  • Anonymous
    August 15, 2012
    @jasonz - do u forget to tag this post, ex. 'favorite feature' ?