65.9K
CodeProject 正在变化。 阅读更多。
Home

学习 Entity Framework (第3天): ASP.NET Web API 2 配合 Entity Framework 6 Code First 迁移

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (6投票s)

2018年10月1日

CPOL

17分钟阅读

viewsIcon

29768

downloadIcon

924

ASP.NET Web API 2 配合 Entity Framework 6 Code First 迁移

目录

引言

在上一篇学习 Entity Framework 的文章 《Entity Framework 学习(第2天):Code First 方法和 Code First 迁移》 中,我们学习了 Code First 方法和 Code First 迁移。在本文中,我们将学习如何使用 ASP.NET Web API2 和 Entity Framework 执行 CRUD 操作。我们将以教程的形式一步一步地设置一个基本的 Web API 项目,并使用 Entity Framework 的 Code First 方法来生成数据库和执行 CRUD 操作。如果您是 Entity Framework 的新手,请按照我之前关于 Entity Framework 数据访问方法解释的文章 《Entity Framework 学习(第1天):数据访问方法》 进行学习。本文将偏重实践,而非理论,以便我们了解如何设置 Web API 项目、Entity Framework 并执行 CRUD 操作。我们不会为这个应用程序创建客户端,而是使用 Postman 这个工具来测试 REST 端点。

路线图

我们将遵循一个五篇文章的系列来详细学习 Entity Framework 的主题。所有文章都将是教程形式,最后一篇除外,我将在其中介绍 Entity Framework 的理论、历史和用法。该系列的议题如下。

Web API

我完全同意来自 Microsoft 文档 的以下摘录。

“HTTP 不仅仅用于提供网页。HTTP 还是构建 API 的强大平台,可以公开服务和数据。HTTP 简单、灵活且无处不在。几乎任何您能想到的平台都有 HTTP 库,因此 HTTP 服务可以触达广泛的客户端,包括浏览器、移动设备和传统桌面应用程序。ASP.NET Web API 是一个在 .NET Framework 之上构建 Web API 的框架。”

您可以在 MSDN 上阅读大量关于 Web API 的理论知识。

实体框架

Microsoft Entity Framework 是一个 ORM(对象关系映射)。来自 Wikipedia 的定义对于 ORM 非常直接且基本不言自明,

“在 计算机科学 中,对象关系映射(ORM、O/RM 和 O/R 映射工具)是一种使用 面向对象 编程语言将数据在不兼容的 类型系统 之间转换的 编程 技术。这实际上创建了一个可以从编程语言内部使用的‘虚拟 对象数据库’。”

作为一个 ORM,Entity Framework 是 Microsoft 提供的数据访问框架,有助于在应用程序中建立对象与数据结构之间的关系。它建立在传统的 ADO.NET 之上,作为 ADO.NET 的包装器,是对 ADO.NET 的增强,以更自动化的方式提供数据访问,从而减少了开发人员在连接、数据读取器或数据集方面的烦恼。它对所有这些进行了抽象,并且在其提供的功能方面更强大。开发人员可以更好地控制他们需要的数据、数据的形式以及数据的数量。没有数据库开发背景的开发人员可以利用 Entity Framework 和 LINQ 的功能来编写优化的查询以执行数据库操作。SQL 或数据库查询的执行将由 Entity Framework 在后台处理,它会处理所有可能发生的事务和并发问题。Entity Framework 提供了三种数据库访问方法,在本教程中,我们将使用这三种方法中的 Code First 方法。

创建 Web API 项目

请按照下面提到的步骤和图片来创建一个 Web API 2 项目。

  1. 本教程使用 Visual Studio 2017。打开 Visual Studio 并添加一个新项目。

  2. 在已安装的模板中选择“Web”选项,然后选择“ASP.NET Web Application (.NET Framework)”。更改解决方案和项目的名称,例如,项目名称可以是“StudentManagement”,解决方案名称可以是“WebAPI2WithEF”。选择 .NET Framework 4.6 作为框架。点击“OK”。

  3. 点击“OK”后,系统会提示您选择 ASP.NET Web 应用程序的类型。选择 Web API,然后点击“OK”。

  4. 点击“OK”后,您将获得一个默认的基本 Web API 项目,其中包含必需的 NuGet 包、文件和文件夹,以及用于运行应用程序的 Views 和 Controllers。

创建模型

我们将创建一个模型类,它将充当 Student 的实体,我们需要对该实体执行数据库操作。为了便于理解其工作原理,我们将保持简单。您可以创建多个模型类,甚至可以在它们之间建立关系。

  1. 右键单击“Models”文件夹并添加一个新类。将该类命名为“Student”。

  2. 将该类设置为 public,并为其添加两个属性:IdNameId 将作为此实体的​​主键。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
     
    namespace StudentManagement.Models
    {
        public class Student
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }
    }
  3. 重新生成解决方案。

添加 API 控制器

让我们添加一个控制器,其中将包含对我们的模型类进行创建、更新、读取和删除的数据库操作。

  1. 右键单击“controller”文件夹,选择添加新 controller 类的选项。

  2. 在下一个提示中,选择使用 Entity Framework 创建带操作的 Web API 2 Controller 的选项。点击“Add”按钮。

  3. 接下来,在 Model 类选项中选择我们创建的模型,即 Student 模型。

  4. 由于我们的应用程序没有数据上下文,请点击 Data context class 选项下拉列表旁边的 + 按钮,并在显示的文本框中输入名称“StudentManagementContext”,然后点击“Add”。

  5. 控制器的名称应为“StudentsController”。点击“Add”完成。

  6. 点击“Add”完成后,它会尝试创建一个控制器脚手架模板,该模板使用 Entity Framework 和我们的模型类包含所有读/写操作。由于我们在添加控制器时的第二步中提到了我们希望控制器使用 Entity Framework 执行数据库操作,因此它会很智能地理解这一点,并添加 Entity Framework 和相关 NuGet 包的引用。创建脚手架模板可能需要一些时间。

  7. 模板生成完成后,您可以在 Web API 项目的“Controller”文件夹中看到添加的 controller 类。此类从 ApiController 类派生,并包含执行 student 实体数据库操作所需的所有方法。如果我们检查方法名称,它们会以前缀 HTTP 动词的名称开头。这是请求被映射到操作的方式。如果您不希望操作以 HTTP 动词为前缀,可以在方法上使用 HTTP 动词属性装饰方法,或者在操作上应用属性路由。我们不会详细讨论这些,而是坚持使用当前的实现。

    Controller 类代码

    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;
    using System.Web.Http.Description;
    using StudentManagement.Models;
     
    namespace StudentManagement.Controllers
    {
        public class StudentsController : ApiController
        {
            private StudentManagementContext db = new StudentManagementContext();
     
            // GET: api/Students
            public IQueryable<Student> GetStudents()
            {
                return db.Students;
            }
     
            // GET: api/Students/5
            [ResponseType(typeof(Student))]
            public IHttpActionResult GetStudent(int id)
            {
                Student student = db.Students.Find(id);
                if (student == null)
                {
                    return NotFound();
                }
     
                return Ok(student);
            }
     
            // PUT: api/Students/5
            [ResponseType(typeof(void))]
            public IHttpActionResult PutStudent(int id, Student student)
            {
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
     
                if (id != student.Id)
                {
                    return BadRequest();
                }
    
                db.Entry(student).State = EntityState.Modified;
     
                try
                {
                    db.SaveChanges();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!StudentExists(id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
     
                return StatusCode(HttpStatusCode.NoContent);
            }
     
            // POST: api/Students
            [ResponseType(typeof(Student))]
            public IHttpActionResult PostStudent(Student student)
            {
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
     
                db.Students.Add(student);
                db.SaveChanges();
     
                return CreatedAtRoute("DefaultApi", new { id = student.Id }, student);
            }
     
            // DELETE: api/Students/5
            [ResponseType(typeof(Student))]
            public IHttpActionResult DeleteStudent(int id)
            {
                Student student = db.Students.Find(id);
                if (student == null)
                {
                    return NotFound();
                }
     
                db.Students.Remove(student);
                db.SaveChanges();
     
                return Ok(student);
            }
     
            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    db.Dispose();
                }
                base.Dispose(disposing);
            }
     
            private bool StudentExists(int id)
            {
                return db.Students.Count(e => e.Id == id) > 0;
            }
        }
    }

Entity Framework Code First 迁移

设想一种场景,您想添加一个新的模型/实体,并且不希望在更新数据库(包含新添加的模型类)时删除或更改现有数据库。Code First 迁移可以帮助您使用新添加的模型类来更新现有数据库,而您的现有数据库将保持不变,包含现有数据。因此,数据和架构不会被重新创建。这是一种 Code First 方法,我们将逐步了解如何在我们的应用程序中启用它。

  1. 打开程序包管理器控制台,并将默认项目选择为您的 WebAPI 项目。键入命令 **Enable-Migrations** 并按 Enter 键。

  2. 命令执行后,它会对我们的解决方案进行一些更改。作为添加迁移的一部分,它会创建一个 Migrations 文件夹,并添加一个名为 **“Configuration.cs”** 的类文件。此类派生自 DbMigrationsConfiguration 类。此类包含一个 Seed 方法,该方法以 context 类为参数,这是我们在 Models 文件夹中生成的。Seed 是一个重写方法,这意味着它包含基类中的一个 virtual 方法,并且派生自 DbMigrationsConfiguration 的类可以重写它并添加自定义功能。如果我们在创建数据库时希望某些表包含一些数据,我们可以利用 Seed 方法向数据库提供种子数据或主数据。

    DbMigrationsConfiguration

  3. 让我们利用这个 Seed 方法,向 Students 模型中添加一些学生。我将添加三个名为 AllenKimJane 的学生。

    Configuration

    using StudentManagement.Models;
     
    namespace StudentManagement.Migrations
    {
        using System;
        using System.Data.Entity;
        using System.Data.Entity.Migrations;
        using System.Linq;
     
        internal sealed class Configuration : 
        DbMigrationsConfiguration<StudentManagement.Models.StudentManagementContext>
        {
            public Configuration()
            {
                AutomaticMigrationsEnabled = false;
            }
     
            protected override void Seed(StudentManagement.Models.StudentManagementContext context)
            {
                context.Students.AddOrUpdate(p => p.Id,
                    new Student { Name = "Allen" },
                    new Student { Name = "Kim" },
                    new Student { Name = "Jane" }
                ); 
            }
        }
    }
  4. context 参数是我们添加控制器时生成的 context 类的实例。我们将其命名为 StudentManagementContext。此类派生自 DbContext 类。此类负责数据库架构,并且该类的 DbSet 属性基本上是我们创建数据库时将拥有的表。它添加了 Students 作为 DbSet 属性,该属性返回我们的 Student 模型/实体,并将直接映射到数据库中将要生成的表。

  5. 下一步是执行名为“Add-Migrations”的命令。在程序包管理器控制台,执行此命令并加上您选择的参数,该参数将是我们第一次迁移的名称。我称之为“Initial”。因此,命令将是 **Add-Migrations Initial**。

  6. 命令执行后,会添加一个新文件,其名称为“Initial”,前面带有日期时间戳。它添加日期时间戳是为了能够跟踪开发过程中添加的各种迁移并将它们区分开。打开文件,我们看到一个名为“Initial”的类,它派生自 DbMigration 类。此类包含两个从 DbMigration 类(即基类)重写的方法。方法名称是 Up()Down()Up 方法用于向数据库添加所有初始配置,并包含 LINQ 格式的创建命令。这有助于生成表以及模型上完成的所有修改。Down 命令与 Up 命令相反。文件中的代码是不言自明的。这里的 Up 命令包含创建 Students 表并将 Id 设置为主键的代码。所有这些信息都来自模型及其更改。

    初始迁移

    namespace StudentManagement.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;
       
        public partial class Initial : DbMigration
        {
            public override void Up()
            {
                CreateTable(
                    "dbo.Students",
                    c => new
                        {
                            Id = c.Int(nullable: false, identity: true),
                            Name = c.String(),
                        })
                    .PrimaryKey(t => t.Id);           
            }
           
            public override void Down()
            {
                DropTable("dbo.Students");
            }
        }
    }
  7. 再次在程序包管理器控制台,运行命令“Update-Database”。

  8. 这是创建数据库和相应表(基于我们的上下文和模型)的最终命令。它执行我们添加的初始迁移,然后从配置类运行 seed 方法。此命令能够智能地检测要运行的迁移。例如,它不会运行之前执行过的迁移,并且每次都会考虑执行所有新添加的迁移来更新数据库。它维护此跟踪,因为数据库首次创建时会包含一个名为 __MigrationHistory 的额外表,该表会跟踪所有完成的迁移。

  9. 命令成功执行后,它会在您的本地数据库服务器中创建数据库,并在 Web.Config 文件中添加相应的连接字符串。连接字符串的名称与我们的上下文类名称相同,这就是上下文类和连接字符串之间关联的方式。

探索生成的数据库

让我们看看在前面的命令成功执行后,我们在数据库中获得了什么。

  1. 由于我们使用了本地数据库,我们可以通过在 Visual Studio 中的“View”选项卡下打开 **Server Explorer** 来打开它。

  2. 显示 Server Explorer 后,我们可以找到生成的 StudentManagementContext 数据库,并且它有两个名为 Students__MigrationHistory 的表。Students 表对应于代码库中的 Student 模型,而 __MigrationsHistory 表正如我之前提到的,是自动生成的表,用于跟踪已执行的迁移。

  3. 打开 Students 表,查看表中添加的初始数据,其中包含我们在 Seed 方法中提供的三名学生姓名。

  4. 打开 __MigrationsHistory 表,查看已执行迁移的行,其中包含上下文键和 MigrationId。添加的 Migration Id 与我们在通过程序包管理器控制台添加迁移时生成的 Initial 类文件名相同。

运行应用程序并设置 Postman

我们的数据库已准备就绪,应用程序也已准备就绪。现在是运行应用程序的时候了。按 F5 从 Visual Studio 运行应用程序。应用程序启动后,您将看到 WebAPI 项目创建时自动存在的 HomeController 启动的默认主页视图。

  1. 设置 Postman。如果您已有 Postman 应用程序,请直接启动它;如果没有,请在 Google 上搜索并安装它。Postman 将充当我们的 Web API 端点的客户端,并帮助我们测试端点。

  2. 打开 Postman 后。您可以从中选择各种选项。我选择第一个选项来创建一个基本请求。并将请求命名为 TestAPI。我们将使用此环境进行所有测试。

端点和数据库操作

我们将测试 API 的端点。StudentsController 的所有操作方法都充当端点,遵循 REST 的架构风格。

在消耗 API 时,会发送一个 HTTP 请求,并返回一个包含返回数据和 HTTP 状态码的响应。HTTP 状态码很重要,因为它们告诉消费者其请求的实际情况;错误的 HTTP 状态码会使消费者感到困惑。消费者应该知道(通过响应)其请求是否得到了处理,如果响应不符合预期,则状态码应告知消费者问题所在,无论是客户级别还是 API 级别。

GET

  1. 当应用程序运行时,这意味着我们的服务已启动。在 Postman 中,通过调用 URL https://:58278/api/students 发送一个 GET 请求来获取学生。当我们点击 **Send** 按钮时,我们看到从数据库返回了所有已添加的 student 的数据。

    此 URL 将指向我们控制器中的 GetStudents() 操作,URL 是 Route.config 文件中定义的路由机制的一部分。在 GetStudents() 方法中,返回的 .Students 表示从数据库返回的所有 students,形式为 IQueryable

    private StudentManagementContext db = new StudentManagementContext();
            // GET: api/Students
            public IQueryable<Student> GetStudents()
            {
                return db.Students;
            }

  2. 可以通过传递学生的 ID 来调用端点以获取单个 student 的详细信息。

    GetStudent(int id) 方法以 student ID 作为参数,返回具有状态码 200 和 student 实体的数据库中的 student。如果未找到,该方法将返回“Not Found”响应,即 404。

            // GET: api/Students/5
            [ResponseType(typeof(Student))]
            public IHttpActionResult GetStudent(int id)
            {
                Student student = db.Students.Find(id);
                if (student == null)
                {
                    return NotFound();
                }
     
                return Ok(student);
            }

POST

我们可以执行 POST 操作将新的 student 添加到数据库。为此,在 Postman 中,选择 HTTP 动词为 POST,URL 为 https://:58278/api/students。在 POST 创建 student 时,我们需要提供想要添加的 student 的详细信息。因此,以 JSON 格式提供详细信息,因为我们在 Student 实体中只有 IdName。此处提供 Id 不是强制性的,因为新 studentId 将在数据库中创建 student 时生成,并且您通过请求提供的 Id 无关紧要,因为 Id 是数据库中的标识列,在添加新实体时将增量 1。在请求的 Body 部分提供新 student 的 JSON。

在发送请求之前,我们还需要设置内容类型的标头信息。因此,在请求的 Headers 部分添加一个名为“Content-Type”且值为“application/json”的新键。根据需要,您还可以在标头部分设置更多键。例如,如果我们正在使用安全的 API,我们将需要传递 Authorization 标头信息,例如授权类型和令牌。我们此处不使用安全的 API,因此提供内容类型信息就足够了。设置标头信息,然后点击 **Send** 以调用请求。

请求发送后,它将路由到控制器中的 PostStudent(Student student) 方法,该方法期望 Student 实体作为参数。它获取我们在请求的 Body 部分传递的实体。请求中 JSON 的属性名称应与我们实体中的属性名称相同。Post 方法执行后,它会在数据库中创建 student,并返回新创建 student 的 ID 以及访问该 student 详细信息的路由信息。

        // POST: api/Students
        [ResponseType(typeof(Student))]
        public IHttpActionResult PostStudent(Student student)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
 
            db.Students.Add(student);
            db.SaveChanges();
 
            return CreatedAtRoute("DefaultApi", new { id = student.Id }, student);
        }

POST 方法执行后,检查数据库中的 Students 表,我们将看到一个新的 Student,姓名为 John,已创建。

PUT

Put HTTP 动词主要用于更新数据库中的现有记录或执行您需要的任何更新操作。例如,如果我们想更新数据库中的一条记录,比如将学生姓名“Akhil”更新为“Akhil Mittal”,我们可以执行 PUT 操作。

在请求中选择 HTTP 动词为 PUT。在 URL 中,提供您想要更新的 studentId,然后在 body 部分提供详细信息,例如更新后的 student 姓名。在本例中为“Akhil Mittal”。

设置 Content-type 标头并发送请求。

请求发送后,它将路由到 API 控制器中映射的 PutStudent() 操作方法,该方法接受 idstudent 实体参数。该方法首先检查传入的模型是否有效?如果无效,则返回 HTTP 状态码 400,即“Bad request”。如果模型有效,它会将传入模型中的 idstudent id 进行匹配,如果不匹配,则再次发送“Bad request”。如果 modelid 都正确,它将更改 model 的状态为“已修改”,以便 Entity Framework 知道此实体需要更新,然后保存更改以将更改提交到数据库。

        // PUT: api/Students/5
        [ResponseType(typeof(void))]
        public IHttpActionResult PutStudent(int id, Student student)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
 
            if (id != student.Id)
            {
                return BadRequest();
            }
 
            db.Entry(student).State = EntityState.Modified;
 
            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!StudentExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
 
            return StatusCode(HttpStatusCode.NoContent);
        }

检查数据库,ID 为 4student 的姓名已更新为“Akhil Mittal”。之前是“Akhil”。

删除

正如其名称所示,delete 动词用于执行数据库中的删除操作。例如,如果我们想删除数据库中的一条记录,比如删除数据库中的学生“John”,我们可以利用此 HTTP 动词。

在请求中将 HTTP 动词设置为 DELETE,并在 URL 中传递需要删除的学生 ID,例如 5 来删除“John”。设置内容类型并发送请求。

由于操作方法的名称,请求会自动路由到 API 控制器的 DeleteStudent() ** 操作方法。该方法接受一个 ID 参数来删除学生。该方法首先执行传入 ID 的 studentget 操作。如果找不到 student,则返回 NotFound() 错误,即 404。如果找到 student,则将其从所有 student 的列表中移除,然后保存更改以将更改提交到数据库。成功交易后,在响应中返回 OK,即 200 状态码。

        // DELETE: api/Students/5
        [ResponseType(typeof(Student))]
        public IHttpActionResult DeleteStudent(int id)
        {
            Student student = db.Students.Find(id);
            if (student == null)
            {
                return NotFound();
            }
 
            db.Students.Remove(student);
            db.SaveChanges();
 
            return Ok(student);
        }

检查数据库,我们看到 ID 为 5student 已被删除。

因此,我们的删除操作也如预期般工作正常。

参考文献

结论

在本文中,我们学习了如何在 Visual Studio 中创建基本的 Web API 项目,以及如何借助 Entity Framework 编写基本的 CRUD 操作。该概念可以用于大型企业级应用程序,您可以在其中利用其他 Web API 功能,如内容协商、筛选、属性路由、异常处理、安全和日志记录。另一方面,可以利用 Entity Framework 的功能,如各种其他数据访问方法、加载等。在此处下载关于 Entity Framework 的完整免费电子书(深入了解 Microsoft .NET Entity Framework)。

© . All rights reserved.