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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (18投票s)

2016年2月6日

CPOL

8分钟阅读

viewsIcon

149386

downloadIcon

2274

通过了解如何使用 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 对象包含 idtitleratinglast-modified-timestampdirectorRating 是一个从一到五的星级枚举,director 是一个 Person 对象,它包含 first-namelast-name。这些类构成了我们应用程序的“模型”。

Model classes

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,项目也能正常工作。

添加模型类

首先,我们将添加模型类,即 MoviePerson 和一个名为 StarRatingenum。最好将这些 .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

这意味着我们应该创建一个名为 MoviesControllercontroller 类。处理各种 HTTP 动词(如 GetPostPut 等)的方法是通过同名方法实现的,例如,要处理 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 有许多过滤器,例如 eqgtlt 等等。让我们尝试 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”。

这是请求的屏幕截图

Creating a new movie

在 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&lt;Movie&gt;(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日:初始版本
© . All rights reserved.