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

使用非常轻量级的堆栈为 CRUD Web 应用程序创建 REST 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (13投票s)

2013年11月8日

CPOL

15分钟阅读

viewsIcon

59869

downloadIcon

1286

本文使用轻量级的服务器和数据库组件,构建了一个 REST 服务器,该服务器向使用 JQuery 或 AngularJS 编写的单页 Web 应用程序返回序列化的 JSON 对象。

引言

本文适合那些开始开发基于浏览器的单页应用程序以管理数据库中的简单对象,但尚未构建服务器来实现 REST 服务的人。创建合适的 REST 服务需要学习很多东西,如果你真正想做的是快速地开始 JQuery 或 AngularJS 的实验,这可能会令人沮丧。因此,我开发了一个由我能找到的最小代码栈组成的 REST 服务器,这样你就无需经历我所经历的。你可以快速浏览本文,下载示例代码,然后继续进行 JavaScript 编写,或者跟着我一起逐步了解每个组件。

背景

当我开始尝试 AngularJS 时,我还没有一个现成的服务器来为我的 Web 应用程序提供 JSON 数据。因此,我不得不寻找合适的服务器技术,并研究实现返回 JSON 的 REST 服务器的约定和代码。开发完成后,弄清楚如何将服务器部署到另一台机器上又花费了更多时间。我真正想做的是开始编写 JavaScript,并继续研究 AngularJS。

LightREST 服务器,包含在下载的代码中并在本文中构建,是我当时正在寻找但从未找到的软件。

示例服务。

我正在开发的示例服务允许一个社交网站为其会员管理一组不同类型的徽章或奖励。徽章的灵感来源于 stackoverflow。徽章有一个标题,一个关于如何获得它的说明,以及一个相应的高/中/低声望级别。该服务将允许你添加、修改或删除徽章,以及根据 ID 获取单个徽章。这是一个人为的例子,你可以将徽章对象替换为你系统中任何你需要持久化列表的简单小对象。

技术栈

下面的每个组件之所以被选中,是因为它不依赖于任何其他东西,唯一的先决条件是 .NET4。这意味着更少的障碍和令人不快的意外,我希望这意味着你可以将你的服务从开发机器部署到内网服务器,而不会遇到通常的麻烦。

  • REST 服务器:Nancy
  • 数据库:SQLite
  • Micro-ORM:PetaPoco
你可能会认为微软提供了更好的替代方案:WebAPI、Sql Express 和 Entity Framework。我不会争辩,但我已经尝试了所有这些,如果你不习惯它们,它们在编程、安装、配置和开发方面就没有那么直接。我假设我的目标读者比对服务器代码更感兴趣的是浏览器代码。

我们将从在 Nancy 中创建可想象的最轻量级的 REST Web 服务器开始,然后使用 SQLite 创建一个数据库文件来存储徽章数据,添加 PetaPoco 来读取数据库,最后将所有这些元素组合在一起,形成完整的徽章服务器。

轻量级服务器

Nancy (http://nancyfx.org) 非常适合这个目的。它被设计为轻量级的,但它非常巧妙地提供了服务器 HTML 或 JSON 的基本管道。它可以自托管或在 IIS 下运行。由于本练习的目标是绝对最少的依赖项,我们将创建一个自托管服务器。

在 VS 2010 或 2012 中创建一个名为 RestServer 的控制台应用程序。下载并向项目添加对 Nancy.dllNancy.Hosting.Self.DLL 的引用。最简单的方法是通过 Nuget,但如果你没有安装它,请从 GitHub (https://github.com/NancyFx/Nancy) 下载 Nancy 源代码,解压并构建解决方案,然后从 Nancy.Hosting.Self 项目的构建文件夹中添加对这两个 DLL 的引用。或者,下载本文附带的代码,并在那里找到 DLL。

通过添加代码来引导 Nancy 并在控制台窗口中启动服务器,在 RestServer 中实现基本服务器。注意:本文和代码使用的是端口号 8088,但端口号的选择是您自己的。

class Program
{
    const string DOMAIN = "https://:8088";
    static void Main(string[] args)
    {
        // create a new self-host server
        var nancyHost = new Nancy.Hosting.Self.NancyHost(new Uri(DOMAIN));
        // start
        nancyHost.Start();
        Console.WriteLine("REST service listening on " + DOMAIN);
        // stop with an <Enter> key press
        Console.ReadLine();
        nancyHost.Stop();
    }
}
public class Bootstrapper : Nancy.DefaultNancyBootstrapper
{
    protected virtual Nancy.Bootstrapper.NancyInternalConfiguration InternalConfiguration
    {
        get
        {
            return Nancy.Bootstrapper.NancyInternalConfiguration.Default;
        }
    }
}

代码非常容易阅读。如果你想更深入地了解它是如何工作的,请查阅 Nancy 网站上的文档。现在我假设你想继续。构建并运行。你应该会看到这个消息:

The Nancy self host was unable to start, as no namespace reservation existed for the provided url(s).
Please either enable CreateNamespaceReservations on the HostConfiguration provided to the
NancyHost, or create the reservations manually with the (elevated) command(s):

netsh http add urlacl url=http://+:8088/ user=Everyone

对于好奇的读者,请参考 http://msdn.microsoft.com/en-us/library/ms733768.aspx 了解更多信息。对于不耐烦的读者,打开一个命令窗口并粘贴上面以 netsh http add urlacl … 开头的行。重新启动服务器后,你应该会看到一个命令窗口确认服务器正在运行并回显基本 URL。尝试将基本 URL 粘贴到浏览器中。它应该显示 404 错误。我们需要增强服务器以响应此和其他 URL 请求。

添加两个类文件来实现将返回内容的类。一个将返回最简单的 HTML,用于你可以从浏览器“ping”你的服务。另一个是用于实际 REST URI 的占位符。

添加一个类文件 HomeModule.cs,其中包含以下代码:

using System;
namespace RestServer
{
    public class IndexModule : Nancy.NancyModule
    {
        public IndexModule() 
        {
            Get["/"] = parameter => { return IndexPage; };
        }

        const String IndexPage = @"
            <html><body>
            <h1>Yep. The server is running</h1>
            </body></html>
            ";
    }
}
Nancy 服务器会检测所有继承自 Nancy.NancyModule 的类,并将其构造函数中指定的所有路由添加到其路由解析器中。此类定义了 GET / 的响应,因此当你输入基本 URL 时,它将返回硬编码的 HTML。

添加一个类 BadgeModule.cs,其中包含以下代码:

namespace RestServer
{
    public class BadgeModule : Nancy.NancyModule
    {
        public BadgeModule() : base("/Badges")
        {
            // https://:8088/Badges/99
            Get["/{id}"] = parameter => { return GetById(parameter.id); };
        }

        private object GetById(int id)
        {
            // fake a return
            return new {Id = id, Title="Site Admin", Level=2};
        }
    }
}
此模块代表 Badges 资源,目前仅实现了一个相当标准的 URL,用于根据 ID 返回资源。此 API 路由在构造函数中定义,在以 GET[“/{id}]开头的行上。注意 {id} 将从 URL 中提取并作为属性添加到由传递给调用实现方法的匿名函数的 parameter 表示的对象中。实现是一个占位符,无论提交什么 ID,它都返回相同的详细信息,但我们稍后会修复它。还有更多的 Nancy 魔力,因为模块代码中不需要 JSON 序列化。Nancy 根据请求头推断响应内容的格式,并为你进行对象序列化。

有了这两个添加,服务器就可以构建和运行了。现在,当你在浏览器中输入 https://:8088/ 时,你应该会收到 HTML 内容。但是,当你尝试 https://:8088/Badges/1 时,你可能仍然会收到错误。在 IE 10 中,你会收到一个 500 错误,其要点是,如果 Nancy 要提供原始数据,它需要一个 Accept 头部来指示应该返回哪种格式的数据。果然,如果你从 IE 切换到 Fiddler (http://fiddler2.com) 并添加以下头部

Accept: application/JSON
到请求中,占位符数据将作为 JSON 返回。如果你不想费心 Fiddler,那么下载中有这样一个简单的控制台项目 RestTester,你可以用来代替。下面是使用 RestTester 程序向服务器发送请求的转录。
K:\>resttester GET https://:8088
OK
<html><body>
<h1>Yep. The server is running</h1>
</body></html>

K:\>resttester GET https://:8088/Badges/1
OK
{"Id":1,"Title":"Site Admin","Level":2}

K:\>resttester GET https://:8088/Badges/2
OK
{"Id":2,"Title":"Site Admin","Level":2}

K:\>resttester GET https://:8088/Badges/3
OK
{"Id":3,"Title":"Site Admin","Level":2}

K:\>resttester DELETE https://:8088/Badges/1
The remote server returned an error: (405) Method Not Allowed.
for: https://:8088/Badges/1
无论 URL 中的 ID 是什么,都会返回相同的数据,并且对于未实现的 delete 方法会返回错误。这看起来很有希望,所以我们暂时搁置服务器,然后检查数据存储端。

轻量级数据存储引擎

你可能已经熟悉 SQL Server 并希望将其用作存储引擎。如果是这样,请跳过本节,然后继续 ORM 部分,在那里将描述如何修改代码以使用 SQL Server 或 Sql Express。否则,我们将使用广为人知的独立数据库引擎 SQLite。在将其引入 Nancy 服务器之前,最好先独立熟悉它,所以我们将创建一个单独的项目,让它工作起来,然后添加 PetaPoco ORM。

在 VS 2010 或 2012 中创建一个名为 SqliteBootstrap 的控制台应用程序(或从源代码打开它)。从 SQLite 下载网站下载并添加 SQLite ADO.NET 数据提供程序的引用,或者搜索 "SQLite ado.net 4.0 provider download",如果链接已更改。请务必选择正确的下载,因为其中许多是纯 C++,而其他一些则要求你安装 VC++ 运行时库。搜索静态构建;例如,查找标题为 "Precompiled Statically-Linked Binaries for 64-bit Windows (.NET Framework 4.0)" 的部分,然后从那里下载。需要添加到引用的 DLL 是 System.Data.SQLite.DLL。或者,直接从示例代码下载中获取版本。

现在将代码添加到 Program.cs 源文件中。主模块将数据库文件设置为 My Documents 文件夹中的 badges.db,并调用一个方法来创建文件。

static void Main(string[] args)
{
    String folder = Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    String fileName = Path.Combine(folder, "badges.db");
    SeedDatabase(fileName);
}
SeedDatabase() 看起来是这样的:
private static void SeedDatabase(string fileName)
{
    String dbConnection = String.Format("Data Source={0}", fileName);
    String sql = @"
create table [Badges] (
[Id] INTEGER PRIMARY KEY ASC,
[Title] varchar(20) ,
[Description] varchar(255),
[Level] int)";
    ExecuteNonQuery(dbConnection, sql);

    sql = @"insert into Badges ([Title], [Description], [Level]) 
             values ('Site MVP', 'Awarded to members who contribute often and wisely', 2);";
    ExecuteNonQuery(dbConnection, sql);
}
该方法创建数据库连接字符串,然后创建一个表(如果数据库文件不存在,还会创建它),并将一个示例记录插入表中。请注意主键字段的定义,这有助于我们在添加 ORM 层时使用。ExecuteNonQuery 方法是从一个通用辅助类(下载中的 SqliteHelper.cs 中的 SQLiteDatabase)中提取的辅助方法。
public static int ExecuteNonQuery(string dbConnection, string sql)
{
    SQLiteConnection cnn = new SQLiteConnection(dbConnection);
    try
    {
        cnn.Open();
        SQLiteCommand mycommand = new SQLiteCommand(cnn);
        mycommand.CommandText = sql;
        int rowsUpdated = mycommand.ExecuteNonQuery();
        return rowsUpdated;
    }
    catch (Exception fail)
    {
        Console.WriteLine(fail.Message);
        return 0;
    }
    finally
    {
        cnn.Close();
    }
}
构建并运行此一次,以在你的文档文件夹中创建数据库文件 Badges.db。可以随意调整文件的位置和名称以满足你的需求。

轻量级 ORM

你可以直接使用 SQLiteDatabase 类来访问数据库,但一个更简洁的解决方案是使用对象关系映射器 (ORM) 来管理从对象到数据库的数据转换。ORM 减少了你的编码工作量,并将数据库层抽象化,以便以后可以切换数据库引擎。下面将演示如何在从 SQLlite 切换到 SQLExpress 时进行修改。我选择 PetaPoco 是因为它在一个源文件中提供了相当不错的基本 ORM 功能。这对于本项目来说很棒,因为 a. 没有额外的组件需要担心安装或部署,b. 它非常容易跟踪,以便弄清楚发生了什么。

从 GitHub (https://github.com/toptensoftware/PetaPoco) 获取 PetaPoco。在下载的文件中搜索 Petapoco.cs,并将其转移到 SqliteBootstrap 项目中。你需要向项目添加对 System.configuration 的引用,因为 PetaPoco 有一个方法可以从 App.config 读取连接字符串(尽管此处未使用)。

ORM 将数据库表中的数据映射到代码中的模型对象。下面是 Badges 表的模型对象类。

 [PetaPoco.TableName("Badges")]
 [PetaPoco.PrimaryKey("Id")]
  public class Badge
  {
        public Int64 Id {get; set;}
        public String Title {get; set;}
        public String Description {get; set;}
        public Int32? Level {get; set;}
  }
PetaPoco 会自动将数据库列映射到对象属性,只要你遵循约定(请参阅 手册页)。Badge 对象在表名中使用复数形式,略微打破了约定,因此有一个属性可以覆盖所有对象名称与数据库表和字段名称直接对应的约定。虽然我们不必定义主键属性,但我认为这样做更安全。请记住,对于数据库表中可以为 null 的日期和数字字段,请使用可空类型,如上面的 Level 属性所示。将此代码添加到项目中,可以在 Program.cs 模块中或单独的类文件中。

现在我们可以添加一个查询数据库的方法。PetaPoco 的优点在于它将 SQL 交给你,你可以直接跟踪到其源代码中,确切地了解发生了什么。

private static void QueryDatabaseOrm(string fileName)
{
    // create a database "context" object
    String connectionString =  String.Format("Data Source={0}", fileName);
    DbProviderFactory sqlFactory = new System.Data.SQLite.SQLiteFactory();
    PetaPoco.Database db = new PetaPoco.Database(connectionString, sqlFactory);
    
// load an array of POCO for Badges
    String sql = "select * from Badges";
    foreach (Badge rec in db.Query(sql))
    {
        Console.WriteLine("{0} {1} {2}", rec.Id, rec.Title, rec.Description);
    }
}
并修改 Main() 以调用它:
static void Main(string[] args)
{
    String folder = Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    String fileName = Path.Combine(folder, "badges.db");
    if (!File.Exists(fileName))
       SeedDatabase(fileName);
    QueryDatabaseOrm(fileName);
}
由于 seeding 方法向数据库添加了一条示例行,现在运行 SqliteBootstrap 应该会显示一行带有该行数据的单行。

将 SQLite 替换为 Sql Server 或 Sql Express

如果你有 Sql Server/Sql Express 并且已经熟悉它们,我建议优先使用它们而不是 SQLite。如果你使用 SQL 创建表,可以使用以下脚本,上面的模型类将是合适的。

create table [Badges] (
[Id] int identity(1,1) NOT NULL ,
[Title] varchar(20) NOT NULL ,
[Description] varchar(255),
[Level] int NOT NULL ,
CONSTRAINT [PK_Badges] PRIMARY KEY CLUSTERED ([Id]) 
)
在代码 QueryDatabaseOrm() 方法中,稍微更改 PetaPoco.Database 对象的构造方式:
    // Replacing SQLite with SQL Server
    // String connectionString =  String.Format("Data Source={0}", fileName);
    // DbProviderFactory sqlFactory = new System.Data.SQLite.SQLiteFactory();
    // PetaPoco.Database db = new PetaPoco.Database(connectionString, sqlFactory);

    // define a standard SQL Server ADO connection string
    String conStr = @"data source=MyServer;initial catalog=MyDb;Trusted_Connection=yes;";
    // Add the SQL Server type to the constructor. PetaPoco will find the provider factory.
    PetaPoco.Database db = new PetaPoco.Database(conStr, "System.Data.SqlClient");
就这样。升级完成!如果你想将连接字符串放在 App.config 文件中,你只需要为 PetaPoco.Database 选择一个备用构造函数。

整合

在介绍了堆栈的各个元素后,是时候将它们整合起来,构建 LightREST Badge 服务了。将 RestServer 项目复制到一个新文件夹并重命名为 LightREST。添加对 Nancy.DLLNancy.Hosting.Self.DLLSystem.Data.SQLite.DLL 的引用,并添加 Petapoco.cs 文件。Program.cs 和 HomeModule.cs 文件无需更改,但 BadgeModule.cs 需要开发以支持 Badges 对象的完整 CRUD REST API;即 GET 用于获取,POST 用于添加,PUT 用于更新,DELETE 用于删除。用以下 URL 路由扩展构造函数,转发到实际实现它们的函数:

public BadgeModule() : base("/Badges")
{
    Get["/{id}"] = parameter => { return GetById(parameter.id); };

    Post["/"] = parameter => { return this.AddBadge(); };

    Put["/{id}"] = parameter => { return this.UpdateBadge(parameter.id); };

    Delete["/{id}"] = parameter => { return this.DeleteBadge(parameter.id); };
}
每个实现方法将检查参数,将数据传输到数据库层并从数据库层返回,并返回带有适当状态码和数据的响应。下面的 POST 实现将作为所需编码的一个示例:
// POST /Badges
Nancy.Response AddBadge()
{
    Badge badge = null;
    try
    {
        // bind the request body to the object via a Nancy module.
        badge = this.Bind<Badge>();

        // check exists. Return 409 if it does
        if (badge.Id > 0)
        {
            string errorMEssage = String.Format("Use PUT to update an existing Badge with Id = {0}", badge.Id);
            return ErrorBuilder.ErrorResponse(this.Request.Url.ToString(), "POST", HttpStatusCode.Conflict, errorMEssage);
        }

        BadgeContext ctx = new BadgeContext();
        ctx.Add(badge);

        // 201 - created
        Nancy.Response response = new Nancy.Responses.JsonResponse<Badge>(badge, new DefaultJsonSerializer());
        response.StatusCode = HttpStatusCode.Created;
        // uri
        string uri = this.Request.Url.SiteBase + this.Request.Path + "/" + badge.Id.ToString();
        response.Headers["Location"] = uri;

        return response;
    }
    catch (Exception e)
    {
        String operation = String.Format("BadgesModule.AddBadge({0})", (badge == null) ? "No Model Data" : badge.Title);
        return HandleException(e, operation);
    }
}
这段代码中需要注意的点是:
  1. 使用 Bind 方法从请求中 POST 的 JSON 自动填充对象。这是更多的 Nancy 魔力。
  2. 严格执行 REST 规则,POST 表示添加。这意味着如果 POST 的数据包含非零 ID,则调用者可能正在尝试更新,而应该使用 PUT 动词。该方法返回一个推荐的 HTTP 状态码,表示操作未完成。
  3. 数据库层通过 BadgeContext 类访问。Context 这个词出现在其他 ORM 框架的代码中。BadgeContext 是 PetaPoco 的一个轻量级包装器。
  4. 如果 Badge 已添加,则返回的状态码设置为 201 (Created),而不是通常的 200 成功码)。
  5. 除了成功码之外,还返回了创建的资源,包括其新分配的 ID 和可通过此服务访问它的 URL。这是通过 Badge 对象创建 JSON 响应时完成的。
  6. 所有异常都会被捕获并返回 500(内部服务器错误)给调用者,但应将尽可能多的错误细节记录到服务器上(良好的错误日志记录非常重要,但超出了本文的范围)。
最后一步是实现 BadgeContext 类。这是 PetaPoco ORM 的一个轻量级包装器,但将其分开,以便可以轻松地切换到替代 ORM(Entity Framework 或 NHibernate)。
public class BadgeContext
{
    public Badge GetById(int id)
    {
        String sql = "select * from Badges where Id =" + id.ToString();
        return BadgeContext.GetDatabase().FirstOrDefault<Badge>(sql);
    }
    public void Add(Badge badge)
    {
        BadgeContext.GetDatabase().Insert(badge);
    }
    internal void update(Badge badge)
    {
        BadgeContext.GetDatabase().Update(badge);
    }
    internal void delete(Badge badge)
    {
        BadgeContext.GetDatabase().Delete(badge);
    }
    private static PetaPoco.Database GetDatabase()
    {
        // A sqlite database is just a file.
        String fileName = Path.Combine(Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments), "badges.db");
        String connectionString = "Data Source=" + fileName;
        DbProviderFactory sqlFactory = new System.Data.SQLite.SQLiteFactory();
        PetaPoco.Database db = new PetaPoco.Database(connectionString, sqlFactory);
        return db;
    }
}
如果你选择了 SQL Server 并且使用了 SQL Server 提供程序和连接字符串,请记住使用它们。数据库错误将由使用 BadgeContext 类的例程捕获。

所有代码就这些了。

测试服务。

在开始你的 JQuery 或 AngularJS 开发之前,我建议学习使用 Fiddler 进行测试。或者,如果你赶时间,可以使用下载中的 RestTester 应用程序。两者都可以用来发出 POST 和 PUT 请求,只要你 POST 或 PUT 的数据格式是 Badge 对象的正确 JSON 表示形式。[顶级技巧要查看正确格式的最简单方法是获取现有记录(记住 SQLite 数据库已使用单个记录进行 seeding),然后编辑返回的数据]。从 JSON 中删除 Id 属性,然后将其保存到文件(使用 RestTester 时)或将字符串粘贴到 Fiddler 的内容窗口中。下面是 RestTester 控制台会话的转录,以说明我的意思。

[a] K:\>resttester GET https://:8088/Badges/1 >json.txt

[b] K:\>notepad json.txt
[make edits to json.txt and save]

[c] K:\>resttester POST https://:8088/Badges json.txt
Created
{"Id":20,"Title":"Watcher","Description":"Todo: add a description","Level":3}
[make another edit to json.txt and save]

[d] K:\>resttester PUT https://:8088/Badges/20 json.txt
No Content

[e] K:\>resttester GET https://:8088/Badges/20
OK
{"Id":20,"Title":"Watcher","Description":"You have visited many times, but never posted","Level":3}
[a] 请求 ID 为 1 的 Badge,并将数据通过管道传输到文本文件。
[b] 编辑文本文件以创建一个新的 Badge。
[c] 将数据 POST 到服务器(URL 中没有 ID)。
[d] 编辑数据文件将“Todo”替换为正确的描述。注意新 Badge 的 ID,并在 PUT 更改时将其添加到 URL 中。
[e] 获取新的 Badge,以验证更改已在服务器上保存。

将其从你的机器上移开。

由于我不是专业的构建和部署工程师,LightREST 服务器设计用于 XCOPY 部署;即无需 msi 文件、注册表键、必需的系统文件、特定配置的服务等。只需 LightREST 构建文件夹中的四个程序集(可选)SqliteBootStrap 程序集来创建数据库;再加上 RestTester 应用程序。下载项目包含一个名为 Deployment 的文件夹,在发布模式下构建时,程序集将被复制到该文件夹中。该文件夹还包括将这些文件复制到另一台机器上所需执行的详细步骤。如果你需要设置一个服务器来演示你的超酷 AngularJS 单页 Web 应用程序,这应该会让你有一个无缝的体验。

结论

这是一段漫长的旅程,但到目前为止,你已经发现了:

  • 你不需要 ASP.NET、WCF 等的强大功能来构建一个基本的 REST 服务器,用于与 Javascript AJAX 库进行交互。
  • 你可以使用真正的独立数据库引擎,在部署到另一台机器时无需特殊安装或配置。
  • 单个文件微型 ORM 是一个很棒的概念,它抽象了将简单的 C# 对象持久化到数据库的许多繁琐工作,并使用了易于跟踪和理解的代码。
该项目不适合用于生产系统。正如我在引言中所述,它旨在为你提供一种快速实现 REST 服务器的方法,为浏览器中使用 Javascript 运行的 CRUD Web 应用程序提供数据。一旦你在这个技术栈上实现了你的概念验证应用程序,就可以相当容易地用更工业级的组件替换每个元素。读者需要自行添加安全、日志记录和 API 版本控制等功能。

© . All rights reserved.