使用 oData V4 和 ASP.NET Web API 进行 CRUD






4.80/5 (18投票s)
通过了解如何使用 ASP.NET Web API 实现 CRUD 方法,快速介绍 oData (v4)。
引言
如果您正在阅读本文,您很可能已经了解了 oData - 它是关于什么以及为什么您想使用它。OData 是一个查询信息的标准,构建在 REST 之上。它标准化了某些查询操作,例如限制返回对象的数量、执行分页、计算返回的元素、根据条件选择对象等等。我们将专注于使用 OData V4 和 ASP.NET Web API 实现对资源的 CRUD 操作。
背景
OData 是一个开放协议,并且得到了 SalesForce、Netflix 等众多信息提供商的支持。有一些关于 oData 的优秀入门文章,例如这篇。本文中的代码非常简单,即使您对 oData 不太了解,但熟悉 REST 甚至 Web 服务,您也应该能够理解使用 oData 有多么简单。
Using the Code
对于本文,我们将通过 oData 公开电影。一个 Movie
对象包含 id
、title
、rating
、last-modified-timestamp
、director
。Rating
是一个从一到五的星级枚举,director
是一个 Person
对象,它包含 first-name
和 last-name
。这些类构成了我们应用程序的“模型”。
OData 服务可以使用 WCF 或 ASP.NET Web API 编写。在本文中,我们将使用 ASP.NET Web API。我们将项目命名为 ODataMovies
,并将数据通过 uri /odata 提供。这意味着所有端点的地址都将是:https:///odata/。
创建项目
启动 Visual Studio 2015 并创建一个新的 Web 项目。在接下来的屏幕中,选择
- 从 ASP.NET 4.5.2 模板中选择“空白”。
- 取消选中“托管在云中”复选框。
- 取消选中“Web Forms”和“MVC”复选框。
- 仅选择 Web API 复选框。
安装 oData Nuget 包
从主菜单中,选择“工具” -> “NuGet 包管理器” -> “程序包管理器控制台”。在 PM> 提示符下,输入
Install-Package Microsoft.AspNet.OData -Version 5.8.0
我们在这里显式指定了版本。您可能不需要这样做。截至撰写本文时,5.8.0 是最新版本,如果您不显式指定版本,将会安装此版本。话虽如此,oData 团队似乎在更改一些类,甚至移除一些类,例如早期版本中存在的 EnititySetManager
,因此锁定版本将确保即使发布了新版本的 oData
,项目也能正常工作。
添加模型类
首先,我们将添加模型类,即 Movie
、Person
和一个名为 StarRating
的 enum
。最好将这些 .cs 文件创建在 Models 文件夹内,尽管这不是必需的。
// Person.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Models
{
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
// StarRating.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Models
{
public enum StarRating
{
OneStar,
TwoStar,
ThreeStar,
FourStar,
FiveStar
}
}
// Movie.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public StarRating Rating { get; set; }
public Person Director { get; set; }
public DateTime LastModifiedOn
{
get { return m_lastModifiedOn; }
set { m_lastModifiedOn = value; }
}
public Movie CopyFrom(Movie rhs)
{
this.Title = rhs.Title;
this.ReleaseDate = rhs.ReleaseDate;
this.Rating = rhs.Rating;
this.LastModifiedOn = DateTime.Now;
return this;
}
private DateTime m_lastModifiedOn = DateTime.Now;
}
}
我们将只通过 oData
服务公开 Movie
对象。
启用 oData 路由
路由是指理解 URL 格式并将其转换为方法调用。例如,如果有人访问 https:///odata/Movies,这应该会触发一个方法,我们可以在其中编写代码来访问电影对象并使其可供客户端使用。此路由设置在 App_Start/WebApiConfig.cs 文件中完成。将 Register
方法中的现有代码替换为
// WebApiConfig.cs file
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Movie>("Movies"); // We are exposing only Movies via oData
config.MapODataServiceRoute
("Movies", "odata", modelBuilder.GetEdmModel()); // Specify the routing
/* Old Stuff which we don't need
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
*/
}
编写业务层
我们将有一个精简的业务层,它将提供存储和检索模型对象(即电影对象)的服务。
创建一个名为 Business 的新文件夹。创建一个名为 DataService.cs 的新文件。我们不会真正地将数据存储在数据库中,而是只使用一个内存中的模型对象列表。
// DataService.cs file
using ODataMovies.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Business
{
public class DataService
{
public List<Movie> Movies
{
get { return m_movies; }
}
public Movie Find(int id)
{
return Movies.Where(m => m.Id == id).FirstOrDefault();
}
public Movie Add(Movie movie)
{
if (movie == null)
throw new ArgumentNullException("Movie cannot be null");
if (string.IsNullOrEmpty(movie.Title))
throw new ArgumentException("Movie must have a title");
if (m_movies.Exists(m => m.Title == movie.Title))
throw new InvalidOperationException("Movie already present in catalog");
lock(_lock)
{
movie.Id = m_movies.Max(m => m.Id) + 1;
m_movies.Add(movie);
}
return movie;
}
public bool Remove(int id)
{
int index = -1;
for (int n=0; n < Movies.Count && index == -1; n++)
if (Movies[n].Id == id) index = n;
bool result = false;
if (index != -1)
{
lock(_lock)
{
Movies.RemoveAt(index);
result = true;
}
}
return result;
}
public Movie Save(Movie movie)
{
if (movie == null) throw new ArgumentNullException("movie");
Movie movieInstance = Movies.Where(m => m.Id == movie.Id).FirstOrDefault();
if (movieInstance == null)
throw new ArgumentException(string.Format
("Did not find movie with Id: {0}", movie.Id));
lock (_lock)
{
return movieInstance.CopyFrom(movie);
}
}
private static List<Movie> m_movies = new Movie[]
{
new Movie { Id = 1, Rating = StarRating.FiveStar,
ReleaseDate = new DateTime(2015, 10, 25),
Title = "StarWars - The Force Awakens",
Director = new Person { FirstName="J.J.", LastName="Abrams" } },
new Movie { Id = 2, Rating = StarRating.FourStar,
ReleaseDate = new DateTime(2015, 5, 15),
Title = "Mad Max - The Fury Road",
Director = new Person { FirstName ="George", LastName="Miller" } }
}.ToList();
private object _lock = new object();
}
}
启用 oData (REST) 方法
为了服务 oData 请求,我们需要创建一个控制器类。控制器负责管理一个资源,对我们来说,资源是电影集合。当我们注册路由时,以下行公开了电影资源
modelBuilder.EntitySet<Movie>("Movies"); // We are exposing only Movies via oData
这意味着我们应该创建一个名为 MoviesController
的 controller
类。处理各种 HTTP 动词(如 Get
、Post
、Put
等)的方法是通过同名方法实现的,例如,要处理 Post 动词,请编写一个名为 Post
的方法。
让我们先公开 movie
集合。创建一个名为 MoviesController
的类,它继承自 ODataController
。
// MoviesController.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using ODataMovies.Models;
using ODataMovies.Business;
using System.Web.OData;
using System.Web.Http;
using System.Net;
using System.Diagnostics;
namespace ODataMovies.Controllers
{
public class MoviesController : ODataController
{
[EnableQuery]
public IList<Movie> Get()
{
return m_service.Movies;
}
private DataService m_service = new DataService();
}
}
通过按 F5 编译并运行项目。在浏览器中输入以下 URL:https://:32097/odata/Movies
注意:将 32097 端口替换为您自己的端口。您应该看到一个 JSON 响应,如下所示
{
"@odata.context":"https://:32097/odata/$metadata#Movies","value":[
{
"Id":1,"Title":"StarWars - The Force Awakens",
"ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FiveStar","Director":{
"FirstName":"J.J.","LastName":"Abrams"
},"LastModifiedOn":"2016-01-26T13:29:10.2039858+05:30"
},{
"Id":2,"Title":"Mad Max - The Fury Road",
"ReleaseDate":"2015-05-15T00:00:00+05:30","Rating":"FourStar","Director":{
"FirstName":"George","LastName":"Miller"
},"LastModifiedOn":"2016-01-26T13:29:10.2044867+05:30"
}
]
}
能够如此轻松地公开数据真是太酷了。不仅如此,试试这个
https://:32097/odata/Movies?$top=1
您只会看到一条记录。
OData
有许多过滤器,例如 eq
、gt
、lt
等等。让我们尝试 eq
过滤器。在 Web 浏览器中输入以下 URL
https://:32097/odata/Movies?$filter=Title eq 'Mad Max - The Fury Road'
结果将匹配标题为“Mad Max - The Fury Road
”的电影。这种魔法是由我们正在使用的 oData
库实现的。由于我们使用了 [EnableQuery]
属性,oData
库会将一个过滤器添加到结果集中。当使用 EF (Entity Framework) 时,最好返回 IQueryable
以利用延迟执行和查询优化等功能。
检索特定项
检索特定项也由 GET
动词处理,我们只需要 URL 中的一些额外信息。在 controller
类中添加以下代码
public Movie Get([FromODataUri] int key)
{
IEnumerable<Movie> movie = m_service.Movies.Where(m => m.Id == key);
if (movie.Count() == 0)
throw new HttpResponseException(HttpStatusCode.NotFound);
else
return movie.FirstOrDefault();
}
要调用此 URL,请使用类似以下的 URL:https://:32097/odata/Movies(1)。其中,1
是 ID 或键。由于我们使用的是 5.8.0 oData
库,因此不一定需要使用 [FromODataUri]
属性,即使您删除它,代码也能正常工作。
添加一项
通过实现 Get
启用了数据获取,让我们来实现添加一个新电影。添加项通常通过使用 POST
动词来处理。数据随 HTTP 请求正文一起发送。由于我们使用的是 5.8.0,因此无需指定 [FromBody]
属性,例如 public IHttpActionResult Post([FromBody] Movie movie)
。
/// <summary>
/// Creates a new movie.
/// Use the POST http verb.
/// Set Content-Type:Application/Json
/// Set body as: { "Id":0,"Title":"A new movie",
/// "ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FourStar" }
/// </summary>
/// <param name="movie"></param>
/// <returns></returns>
public IHttpActionResult Post([FromBody] Movie movie)
{
try
{
return Ok<Movie>(m_service.Add(movie));
}
catch(ArgumentNullException e)
{
Debugger.Log(1, "Error", e.Message);
return BadRequest();
}
catch(ArgumentException e)
{
Debugger.Log(1, "Error", e.Message);
return BadRequest();
}
catch(InvalidOperationException e)
{
Debugger.Log(1, "Error", e.Message);
return Conflict();
}
}
要测试此功能,您需要一个可以发送 HTTP POST
请求并传递标头和内容的应用程序,例如 Telerik 的 Fiddler。可以从 www.telerik.com/fiddler 获取,它是免费的。
构建项目并按 Visual Studio 中的 F5 键运行它。安装 Fiddler 后启动 Fiddler。在 Fiddler 中,执行以下操作
- 点击“Composer”选项卡。复制 Visual Studio 启动的浏览器中的 URL,并将 URL 修改为:https://:32097/odata/Movies,然后将其粘贴到地址文本框中。
- 从下拉菜单中选择“POST”。
- 在标头文本框中输入
Content-Type: Application/JSon
。 - 粘贴
{ "Id":1,"Title":"Transformers - 4","ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FiveStar","Director":{ "FirstName":"Not","LastName":"Sure" } }
到请求正文文本框中。 - 按“Execute”。
这是请求的屏幕截图
在 Fiddler 的左窗格中,您应该会看到您的请求。双击以查看详细信息
需要注意的重要一点是,我们收到了 HTTP 状态码 200,表示“OK”。响应文本包含一个 JSON 对象,这是新创建的电影对象,请查看 ID;它是 3。
实现 PUT
PUT
方法用于更新现有资源。将以下代码粘贴到控制器中以实现 PUT
。
/// <summary>
/// Saves the entire Movie object to the object specified by key (id).
/// Is supposed to overwrite all properties
/// Use the PUT http verb
/// Set Content-Type:Application/Json
/// Set body as: { "Id":0,"Title":"StarWars - The Force Awakens",
/// "ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FourStar" }
/// </summary>
/// <param name="key"></param>
/// <param name="movie"></param>
/// <returns></returns>
public IHttpActionResult Put(int key, Movie movie)
{
try
{
movie.Id = key;
return Ok<Movie>(m_service.Save(movie));
}
catch(ArgumentNullException)
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
catch(ArgumentException)
{
return NotFound();
}
}
在这种情况下,如果我们想传达错误,我们需要抛出带有正确 HTTP 错误代码的 HttpResponseException
。在前面的情况下,我们可以仅使用 NotFound()
等方法返回 HTTP 状态。当您抛出 HttpResponseException
时,Web API 框架无论如何都会将其转换为 HTTP 状态。
让我们使用 Fiddler 和以下 URL 来调用此方法。我们必须小心指定正确的 HTTP 标头,在这种情况下是
Content-Type: Application/Json
https://:32097/odata/Movies(2)
我们需要在请求正文中指定要针对 ID 为 2
的电影更新的属性。即使是未更改的属性也需要指定,因为 PUT
应该盲目地更新所有属性。在此示例中,让我们尝试为 Mad-Max
提供 FiveStar
。在请求正文中使用以下文本
{
"Id":2,"Title":"Mad Max - The Fury Road","ReleaseDate":"2015-05-15T00:00:00+05:30",
"Rating":"FiveStar","Director":{
"FirstName":"George","LastName":"Miller" }
}
这是 Fiddler 的 composer 中请求外观的屏幕截图
如果一切顺利,响应应该是相同的电影,所有属性都已更新。
实现 DELETE
Delete
的实现方式相同。请注意,如果找不到键,我们将返回适当的 HTTP 状态码,即 NOT FOUND
。
/// <summary>
/// Use the DELETE http verb
/// Request for odata/Movies(1)
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public IHttpActionResult Delete(int key)
{
if (m_service.Remove(key))
return Ok();
else
return NotFound();
}
要测试 DELETE
,请使用 Fiddler 并选择“DELETE
”动词。我们需要通过 ID 指定要删除的对象。这可以通过以下格式的 URL 完成:https://:32097/odata/Movies(2)
。这将删除 ID 为 2
的电影。
实现 PATCH
我们将实现的最后一个方法是 PATCH
。此方法类似于 PUT
,但不同之处在于,在 PUT
中,传递对象的所有属性都会被复制到现有对象中,而在 PATCH
中,只有已更改的属性会应用于现有对象。属性更改使用 Delta
类对象传递。这是一个模板类,其方法名为 CopyChangedValues()
,用于将已更改的属性复制到目标对象。
/// <summary>
/// Use the PATCH http Verb
/// Set Content-Type:Application/Json
/// Call this using following in request body: { "Rating":"ThreeStar" } ///
/// </summary>
/// <param name="key"></param>
/// <param name="moviePatch"></param>
/// <returns></returns>
public IHttpActionResult Patch(int key, Delta<Movie> moviePatch)
{
Movie movie = m_service.Find(key);
if (movie == null) return NotFound();
moviePatch.CopyChangedValues(movie);
return Ok<Movie>(m_service.Save(movie));
}
要调用 patch
方法,请使用 URL https://:32097/odata/Movies(1)
。
这将把属性应用到 ID 为 1
的电影上。
在 Fiddler 中将 content-type
设置为 Application/Json
并选择“PATCH
”动词。
如果成功,我们将返回 HTTP 状态码 OK (200),并在响应正文中返回整个对象(JSON 格式)。
摘要
总之,oData 很好地规范了公开资源和支持 CRUD 操作。oData
库提供了有用的过滤选项,您可以通过在控制器方法上使用 [EnableQuery]
属性免费获得这些选项。
历史
- 2016年2月6日:初始版本