我最喜爱的功能:Entity Framework Code First和 ASP.NET Web API

[原文发表地址] My Favorite Features: Entity Framework Code First and ASP.NET Web API

[原文发表时间] 2012-07-23 11:59

这是一个Entity Framework令人激动的时刻!上周四,团队宣布了一个开放源码的版本,现在它在Entity Framework CodePlex 网站上是可用的。我一直在我个人的开发中使用Entity Framework,我将利用这个机会来使用有关EF的博文继续我的"最爱的功能"系列

在这篇帖子中,我将使用Entity Framework中的Code First工作流来构建一个应用程序。Entity Framework原先是作为.NET Framework 中的一部分而推出的,但在几经努力之后,已经在NuGet中推出了最后几个版本,位于.NET Framework的中间版本。除了在 NuGet中可用之外,Entity Framework (EF5) 的最新版本也包含在 Visual Studio 2012 中的很多地方。

作为构建应用程序的一部分,我也会使用ASP.NET Web API 来构建 一个HTTP 服务。ASP.NET Web API 是我另一个最喜爱的功能,是创建由不同客户端使用的服务的杰出方式。

EF5 中的新增功能

我想我会首先简要概述在Entity Framework 5 中的一些新功能,我们将在这篇文章中使用其中的一些。我一直在盼望其中的几个功能,并很高兴看到在最新的版本中添加了它们。

Entity Framework 5 中的新功能:

  • 枚举支持是EF 久等的功能,并让您在您的域类中拥有枚举属性。EF5 让枚举支持在 EF 设计器和Code First中是可用的。
  • 现在,现有数据库中的表值函数可以包含在使用 EF 设计器所创建的模型中。
  • 现在您可以使用DbGeography 和 DbGeometry 类型来在你的模型中公开空间数据类型。空间数据可以包含在由EF 设计器或Code First所创建的模型中。
  • EF5 还包括一些重大的性能改进,你可以在ADO.NET 的博客中找出更多有关内容。

包括在 Visual Studio 2012中的EF 设计器,它也有一些新的功能:

  • 现在模型可以被分为多个关系图,在使用大型的模型时,这是很不错的。您还可以应用着色到实体中来帮助您识别您的模型的分区。
  • 改进了转置模型的向导,使其更容易和更快地为查询数据批量导入存储过程

创建应用程序

EF5在 Visual Studio 2012 中出现的地方之一是 ASP.NET Web API 的项目模板,因此,如果您创建了一个新的 ASP.NET Web API 项目,默认情况下,在您的项目中,Entity Framework 5是可用的。

在这篇帖子中,我就会创建一个简单的 web 应用程序来帮助人们找到有趣的旅游景点。我将会创建一个 web 页面以帮助他们找到给定位置最接近的旅游胜地。而不是让web 页面直接与数据库进行通信,我将创建一个 Web API 来公开数据,以便各种平台上的应用程序也可以访问该数据。

我将由创建一个新的 MVC 4 应用程序来开始,指向.NET 4.5,并选择 ‘Web API’模板。

NewProject

建立一个模型

Code First了不起的事情之一是建立模型就如同定义一组类那么简单。如果需要的话,您还可以提供额外的配置来定义数据库和您的类映射到数据库的方式。

在我的项目的Models文件夹中,我将添加一个 Models.cs 文件,其中包含了下面的类。

    1: using System.Collections.Generic;
    2: using System.Data.Spatial;
    3:  
    4:   
    5: namespace TouristAttractions.Models 
    6: {
    7:  
    8:     public class TouristAttraction
    9:     {
   10:         public int TouristAttractionId { get; set; }
   11:         public string Name { get; set; }
   12:         public DbGeography Location { get; set; }
   13:  
   14:  
   15:         public List<Review> Reviews { get; set; }
   16:     }
   17:  
   18:  
   19:     public class Review
   20:     {
   21:  
   22:         public int ReviewId { get; set; }
   23:         public string Author { get; set; }
   24:         public string Comments { get; set; }
   25:  
   26:  
   27:         public int TouristAttractionId { get; set; }
   28:         public TouristAttraction TouristAttraction { get; set; }
   29:  
   30:     } 
   31: }

添加 一个API 控制器

既然我有一个模型了,我要创建一种让我可以读取和写入旅游景点的 API 控制器。API 控制器类似于一个 MVC 控制器,除了它是作为一项服务使用,而不是挂接到一组试图。

在添加一个控制器之前,我需要首先生成该应用程序。下一步我就会右击Controllers文件夹,然后从上下文菜单中选择Add-> Controller … … '。我将新的控制器命名为 'AttractionsController',然后选择‘API controller with read/write actions, using Entity Framework’模板。此模板将生成一段使用Entity Framework所读取和写入我的TouristAttraction 类实例的代码。

我还可以指定我想要为数据访问使用哪些Entity Framework上下文。上下文是一个类,代表与数据库的会话,并让我可以读取和写入数据。我还没有创建一个上下文,所以我就直接从列表中选择 <New data context….>, 这将为我创建一个新的上下文(我称之为 TourismContext)。

AddController

当我完成了添加控制器向导时,一些项就被添加到我的项目中。我要求向导所创建的TourismContext 已添加到了Models文件夹中。此类来自 DbContext,它是Entity Framework的主要API。此上下文还为每个我想要访问的类型公开了 DbSet。DbContext 有一套公约来挑选一个数据库试着使用,但 ASP.NET Web API 已覆盖了这些公约,并告诉我的上下文来从config文件中加载其连接信息的(使用构造函数中的"name ="语法)。

    1: public class TourismContext : DbContext 
    2: { 
    3:     public TourismContext() : base("name=TourismContext")
    4:     { 
    5:     }
    6:  
    7:     public DbSet<TouristAttraction> TouristAttractions { get; set; }
    8: }

果然,如果我在 Web.config 中查找,我发现 TourismContext 连接字符串,将我的上下文指向一个称为 TourismContext (后跟一个时间戳,以确保它不会与任何以前的 ASP.NET Web API 项目发生冲突) 的 LocalDb 数据库。

<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>

最后,API 控制器本身已被添加到Controllers文件夹中。在控制器上编码以支持获取所有的 TouristAttractions、用某个关键词查找 TouristAttraction,并添加、 更新和删除 TouristAttractions。为简洁起见,我只将展示所生成的代码的一部分:

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;

    }

...

}

我也会添加一个新的 GetTouristAttraction 方法到 AttractionsController中来定位坐标和查找最接近的景点。

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();

}

使用Web API

因为 ASP.NET Web API 使用基本的 HTTP 通信,我的服务可以由来自任意数量的平台和技术所使用。只举一个例子,我将构建一个简单的 web 页面,该页面从 JavaScript 中访问该服务。

我会用下面的代码替换 Views\Home\Index.cshtml 的内容:

<!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>

 

此页面的主要功能是在搜索功能。当用户指定坐标来搜索时,此函数将使用 HTTP PUT请求来为最接近的景点请求服务。DisplayMap 函数然后使用 Bing 地图来直观地显示数据。

如果我运行该项目,我可以测试是否页面显示正确。但是,我们没有任何数据来显示:

WebPageNoData

现在您可能想知道当我并没有做任何事情来创建时,它是如何连接到数据库的。因为我的数据库不存在,Entity Framework Code First使用一套约定来确定该架构看起来应该像什么样的,并为我创建它。我查看我的 LocalDb 实例,我看到它所创建的新数据库:

Database

放心,如果你不喜欢默认的约定,有大量的选项可用于更改表名、 列名、 数据类型以及几乎创建架构所需的每一个方面。Code First还可用于映射到现有的数据库。这超出了本文的范围,但您可以签出EF 动力工具,然后将它作为一个良好的起点。

播种数据

现在我想要在我的数据库中获取一些种子数据,以便我们可以看到正常的 web 页面。我将使用Entity Framework Code First迁移来执行此操作。迁移功能能够让我随着模型的更改而改进数据库(我们很快就会看到这个实际操作),它也让我可以指定数据库中应该存在的种子数据。

若要开始使用Code First迁移,我可以使用Package Manager Console中的Enable-Migrations命令来打开我的TourismContext 的迁移。Package Manager Console可以从Visual Studio中的Tools-> Library Package Manager菜单打开。

EnableMigrations

启用迁移已添加了几个新的Migrations文件夹到我的项目中,其中包含两项:

·Configuration.cs — — 此文件允许我为如何迁移我的数据库定义设置。这包括迁移生成的文件夹,种子数据应用到数据库的方式,为第三方数据库 (即 MySql)注册供应商 的事情。

·<timestamp> _InitialCreate.cs — — 这是迁移,展现了我已应用到数据库中(即创建TouristAttractions 和Reviews表) 的更改。迁移功能已记录在我的本地数据库中。

我将修改Configuration.cs 来在Seed方法中指定一些种子数据。你会发现我正在使用AddOrUpdate 方法,它可以让我指定一个属性来匹配— — — — 在我的例子中,即为 TouristAttraction 的名称。如果迁移找到具有该名称的现有 TouristAttraction,它将更新其当前值来匹配我所提供的 ;如果没有找到的话,它将插入新的 TouristAttraction。

    1: protected override void Seed(TouristAttractions.Models.TourismContext context) 
    2: {
    3:  
    4:     context.TouristAttractions.AddOrUpdate(a => a.Name,
    5:         new TouristAttraction
    6:         {
    7:  
    8:             Name = "Space Needle, Seattle",
    9:             Location = DbGeography.FromText("POINT(-122.348670959473 47.619930267334)")
   10:         });
   11:  
   12:  
   13:     context.TouristAttractions.AddOrUpdate(a => a.Name,
   14:         new TouristAttraction
   15:         {
   16:             Name = "Pike Place Market, Seattle",
   17:             Location = DbGeography.FromText("POINT(-122.341697692871 47.6094245910645)")
   18:         });
   19:  
   20:  
   21:     context.TouristAttractions.AddOrUpdate(a => a.Name,
   22:         new TouristAttraction
   23:         {
   24:             Name = "Statue of Liberty, NY",
   25:             Location = DbGeography.FromText("POINT(-74.0439682006836 40.6886405944824)")
   26:         });
   27:  
   28: }

现在我可以从Package Manager Console中运行Update-Database命令,种子数据将被应用到我的本地数据库。当我运行该应用程序时,我可以在实际中看到:

WebPage

更改模型

我有一个简单的应用程序可供运行,但随着时间的推移,我的要求有可能会改变。例如,我可能会决定当用户查看一个景点时,我想要让他们提供一个评级。

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; }

}

但是当我更新我的模型时,它不再与Code First所创建的数据库架构相匹配了。如果运行该应用程序,我会得到一个InvalidOperationException",说明“自从该数据库被创建以来,模型备份'TourismContext' 上下文已被更改。请考虑使用Code First迁移来更新数据库"。

我已经启用了迁移来处理数据种子,所以我可以运行Add—Migration AddRating' 命令来将我挂起的更改移到到新的迁移中,并给它一个名称 ('AddRating')。

运行此命令,则会将一个新的 <timestamp> _AddRating.cs 文件添加到Migrations文件夹中。此文件包含一些步骤来使我的数据库架构与当前的模型相匹配:

    1: namespace TouristAttractions.Migrations 
    2: {
    3:     using System;
    4:     using System.Data.Entity.Migrations;
    5:  
    6:     
    7:     public partial class AddRating : DbMigration
    8:     {
    9:         public override void Up()
   10:         {
   11:             AddColumn("dbo.Reviews", "Rating", c => c.Int(nullable: false));
   12:         }
   13:  
   14:         
   15:         public override void Down()
   16:         {
   17:             DropColumn("dbo.Reviews", "Rating");
   18:         }
   19:     }
   20: } 

因为这只是支架式的代码,我可能会对其进行编辑。例如,我可以创建索引,或添加列或表,而这些列和表并不是我的Code First模型的一部分。我还可以使用 Sql 方法来作为迁移的一部分而执行任意 SQL。

此示例中的支架式代码是不错的,所以我将运行Package Manager Console中的Update-Database命令。此命令将检查该数据库以查看哪些迁移已应用、然后与我的项目中的迁移相比较,并应用任何待定的迁移。既然已经更新了架构,那么我可以再次运行应用程序,一切都正常。

结论

我希望这篇文章让你更深入了解Entity Framework Code First和 ASP.NET Web API。若要了解有关Entity Framework,我推荐访问Entity Framework开发人员中心Entity Framework团队博客。至于 Web API,请访问https://www.asp.net/web-api

希望大家喜欢 !

https://twitter.com/jlzander 上追随我的脚步