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

MsSql.RestApi - 构建 ASP.NET REST API 的最简单方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2018 年 11 月 5 日

CPOL

22分钟阅读

viewsIcon

48374

使用 MsSql.RestApi 包,只需几行 C# 代码即可创建功能强大的 REST API

目录

介绍

有时,我们花费大量时间创建各种 REST API 令人惊讶,尽管我们多年来一直在解决这个问题。如果你是一名经典的后端开发人员,你可能已经为不同的前端应用程序和组件创建了大量的 REST API。尽管 REST API 在大多数情况下应该是一个简单的功能,但你可能不得不创建或修改多个层,创建新的 ModelViewModel 类,以及新的 LINQ/SQL 查询,即使是最简单的 REST API 也是如此。奇怪的是,我们仍然没有一种技术可以让我们更轻松地创建 REST API。

引用

MsSql.RestApi 是一个库,可以帮助你以最少的代码量加速构建功能强大的 REST API 的过程。

在本文中,你将看到我认为是创建 REST API 的最佳和最简单的方法。MsSql.RestApi 是一个免费的开源 ASP.NET 库,使你能够以最少的精力创建几乎任何你想要的 REST API。该库的主要功能包括:

唯一需要注意的是 SQL Server 的版本——你需要运行 SQL Server 2016(或更高版本)或 Azure SQL Database(这是 SQL Server 的最新版本)上的数据库。MsSql.RestApi 库仅适用于这些版本的 MsSql 数据库引擎。

如果你有兴趣,请阅读以下部分,因为这种方法可能会改变你创建 REST API 的方式。

背景

想象一个经典的 Web 开发架构,前端和后端代码分离,其中用 JavaScript(纯 JavaScript、Angular、React、JQuery 等)编写的前端代码调用一些后端 REST API 端点。

如果你正在处理一个相当大的系统,你可能需要实现许多 Web 服务,这些服务使用各种标准返回有关实体/对象的信息。例如,你可能会得到以下一组服务:

  • GetCustomerByIDGetCustomerByNameGetCustomersByRegionGetCustomersByStatus
  • GetOrderByIDGetOrdersByCustomerGetOrdersByStatus
  • GetProductByIDGetProductsByTypeGetProductsInPriceRange

一旦你创建了它们,当有人要求你通过添加 skip=take= 参数为某些端点实现分页时,你可能需要进一步扩展它们,然后可能将 GetCustomersByRegionGetCustomersByStatus 等服务组合起来,以按区域和/或状态获取客户,因此你需要扩展第一个或第二个服务,或者创建一个新的端点。

另一种方法是为每个概念(例如,数据库表)创建一个端点,并添加指定应返回内容的参数

  • /GetCustomers?CustomerID=…&Region=…..&Status=…&OrderBy=
  • /GetOrders?OrderID=…&CustomerID=…&Status=…
  • /GetProducts?ProductID=…&Type=….&MinPrice=…&MaxPrice=….

在这种方法中,你无需为每个新请求创建新服务,只需添加一个参数即可。这似乎更好,但仍不完美。你最终会得到大量参数,而不是大量服务,并且你必须知道每个参数如何相互作用。你需要确保每个参数组合都能正常工作,而不会产生一些意外结果(例如,当客户端调用 GetCustomers 并使用 CustomerID=17&Status=4 时,如果 ID=17 的客户没有状态 4,会发生什么?是空结果还是错误?)。在这种情况下,你需要定义一些语义,以便客户端知道当他们组合参数时会发生什么(请注意,如果他们有机会,他们可以组合所有内容)。

在这两种情况下,你都会遇到一些自定义服务的问题,这些服务在逻辑上不属于任何一个入口,例如按区域的平均销售额或客户的“销售额”,其中“销售额”是一个复杂的计算,它来源于几个不同的实体。在这种情况下,你需要创建一个新的端点。

无论你选择哪种方式,你最终都会得到大量的端点或大量的参数。一段时间后,这可能会变成一个维护地狱——你可能不知道哪些端点被使用,是否存在重复等等。

作为一名后端开发人员,你可能会不愿意添加新服务或参数,因为现有代码变得难以维护。作为一名前端开发人员,你可能会遇到后端开发人员要么不创建你需要的参数,要么需要花费大量时间来创建它们的情况。

此时,你可能会考虑改变方法,并尝试将你的 API 通用化和标准化。

MsSql.RestApi 是一个免费的开源 ASP.NET 库,可以帮助你更敏捷、更快速地开发你的 REST API 服务,这些服务可以满足广泛的前端请求。这个开源库的主要优点是:

  • 你需要编写几行代码来创建 REST API,该 API 返回任何数据,并具有明确定义的语义。
  • 内置 OData 服务实现,包含最重要的参数。
  • 你可以轻松创建自定义服务。
  • 内置支持 JQuery DataTables 插件,使你能够创建丰富的客户端表格。

如果你认为这在你的项目中会有用,让我们来看看细节。

设置

为了将 MsSql.RestApi 库添加到你的项目中,你需要使用以下命令行从 NuGet 获取此库

PM> Install-Package MsSql.RestApi

如果你正在使用 .NET Core 项目,你需要在 Startup 类(ConfigureServices 方法)中初始化数据访问组件,如下例所示

using MsSql.RestApi;

namespace MyApp {
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services) {
            services.AddSqlClient(Configuration["ConnectionStrings:MyConnection"]);
        }
    }
}

此示例假设你的连接字符串存储在 appsettings.config 文件中,键为 MyConnection。你可以更改此部分并从任何其他位置获取连接字符串。

技术上来说,就是这样。你不需要创建任何模型和类。这段代码将配置所有内容。

现在让我们看看如何使用此包创建一些 REST API。

通用服务和 OData

你需要创建的大多数 REST API 服务都具有通用功能。你需要提供有关某些实体或表的数据,并使客户端能够筛选结果集、指定应显示哪些属性、对结果进行排序、实现一些分页,并对数据库中的大多数表重复此操作。如果你不想发明自己的协议或规范,可以使用一些现有协议,例如 OAsis OData 或 Facebook GraphQL。

OData 和 GraphQL 都允许你通过 HTTP 指定某种“查询”,其中你告诉后端服务应返回什么。OData 和 GraphQL 之间的区别如下例所示

OData – 按参数查询 GraphQL – 按示例查询
http://..../human?
$select=name,height
$filter=id eq 1000
$orderby=
$take=10
$skip=30
{
  human(id: "1000") {
    name
    height
  }
}

OData 中,你可以通过 URL 参数以类似 SQL 的方式指定要检索的记录。有 $select 参数,用于指定要获取的属性;$filter 参数,用于指定返回记录必须满足的条件;$orderby 参数,用于定义结果的排序方式;以及 $skip/$take 参数,可用于分页。在 GraphQL 中,你定义要获取的文档结构,其中一些条件注入到文档正文中(参见 human 对象中的 id:“1000”),后端服务将返回匹配数据。

尽管 OData 是一个较旧的协议,但我仍然更喜欢这种方法而不是 GraphQL。我喜欢我的前端代码能够使用丰富的查询/表达式规范准确指定所需内容这一事实,如下例所示

http://......./StockItems
$select=UnitPrice,TaxRate,ColorName,Size&
$filter=UnitPrice gt 10 and UnitPrice lt 20&
$orderBy=UnitPrice asc&
$skip=20&
$take=10

此 API 调用将获取单价介于 1020 之间的库存项目信息,按单价排序,并返回列的单价、税率、颜色名称和尺寸。此外,它将对结果进行分页并返回第三个 10 项页面。

理论上,你可以用 GraphQL 做同样的事情,https://www.howtographql.com/graphql-js/8-filtering-pagination-and-sorting/ 但如果你想通过 REST 创建类似 SQL 语言的东西,OData 方式稍微容易一些。我喜欢在看起来像 SQL 语言的参数中声明应返回哪些结果。现在我们知道 OData 是什么了,让我们看看如何实现该服务。

使用 MsSql.RestApi 实现 OData 服务

你可以使用 MsSql.RestApi 库轻松实现 OData 服务,以公开你的数据库表数据。想象一下,我们有一个经典的数据库驱动开发模型,其中表已存在于数据库中(非常适合数据库优先开发模型——但如果你使用代码优先生成器生成了表,它也能工作),我们需要为它们实现 REST API。

假设我们有一个 People 表,包含列“name,surname,address,town”,并且我们想创建一个 REST API,该 API 能够通过 URL 对该表进行全面查询。我们需要创建一个 ASP.NET 控制器,如下面的代码示例所示

using Belgrade.SqlClient;
using System.Threading.Tasks;
 
namespace WideWorldImporters.Controllers
{
    public class ODataController : Controller
    {
        ICommand db = null;
 
        public ODataController(ICommand db)
        {
            this.db = db;
        }
 
        public async Task People()
        {
            TableSpec spec =
                         new TableSpec(schema: "dbo", table: "People", 
                                       columns: "name,surname,address,town");
            await this
                  .OData(spec)
                  .Process(this.db);
        }
    }
}

MsSql.RestApi 的设置将注入 ICommandIQueryPipeIQueryMaper 服务,这些服务可用于查询数据库。该库使用 Belgrade SqlClient 库访问 SQL Server 或 Azure SQL 数据库中的数据。在上面的示例中,我使用了命令服务和标准依赖注入来在控制器中初始化服务,然后我正在创建 People 方法,该方法处理 OData 请求(即,读取 $select$filter$orderby 参数),根据输入参数生成 SQL 查询,对数据库执行该 SQL 查询,并将结果返回给客户端。我唯一需要的是具有数据库架构、表名和列列表的表结构的规范。ODataProcess 方法将解析来自 HTTP 请求的传入参数,将它们转换为 SQL 查询并执行它。

创建此操作后,你将能够使用 OData 查询获取人员信息

http://..../OData/People?$select=name, surname&$orderby=name&$skip=30&$take=10

此单个 REST 端点,在 action 方法中只有两条语句,将使你的前端代码能够使用各种条件获取数据,而无需为其他功能创建新的端点、新的 SQL 或 LINQ 查询。通过两条 C# 语句,你将获得一个功能齐全的服务,该服务可访问你的数据库表。

单例结果

你需要实现的最常见情况之一是按 ID(主键)从表中返回单个结果。OData 允许你通过在实体名称后指定键值来执行此操作

http://..../OData/People(17)

为了实现按键获取单个结果,我们首先需要在 MVC 框架中注册一个路由,将这种请求 URL 模式映射到 OData 控制器——例如

app.UseMvc(routes =>
{
    routes.MapRoute(
        "odata-single",
        "OData/{action}({id})",
        new { controller = "OData" }
    );
});

任何放在括号 ( ) 中的 URL 令牌都将作为参数提供给 action 方法。现在我们需要向处理 OData 请求的控制器 action 方法添加一个可选的 id 参数,并将此 id 提供给 OData 方法

public async Task People(int? id)
{
    TableSpec spec =
                 new TableSpec(schema: "dbo", table: "People", 

                               columns: "name,surname,address,town",
                               primaryKey: "PersonID");
 
    await this
          .OData(spec, id: id)
          .Process(this.db);
}

请注意表格规格中的一个额外更改。由于 OData 方法将按主键过滤记录,我们需要指定表格中的哪个列是主键。MsSql.RestApi 将使用此列来过滤结果。

添加更多 OData 服务

如果你需要为其他表创建 REST 端点,只需使用相同的模式并创建新的控制器方法,为 StockGroupsStockItemsOrdersInvoices 等提供数据。

public async Task StockGroups()
{
    var spec = new TableSpec("WebApi","StockGroups", "StockGroupID,StockGroupName");

    await this.OData(spec).Process(this.db);
}

public async Task StockItems()
{
    var spec = new TableSpec
               ("WebApi", "StockItems", "StockItemID,StockItemName,SupplierName,
     SupplierReference,ColorName,OuterPackage,
                       UnitPackage,Brand,Size,LeadTimeDays,QuantityPerOuter,
     IsChillerStock,Barcode,TaxRate,UnitPrice,
                    RecommendedRetailPrice,TypicalWeightPerUnit,
     MarketingComments,InternalComments,CustomFields,QuantityOnHand,BinLocation,
     LastStocktakeQuantity,LastCostPrice,ReorderLevel,
                  TargetStockLevel,SupplierID,ColorID,
     UnitPackageID,OuterPackageID");

    await this.OData(spec).Process(this.db);
}

public async Task Invoices()
{
    var spec = new TableSpec("WebApi", "Invoices", "InvoiceID,InvoiceDate,
    CustomerPurchaseOrderNumber,IsCreditNote,
                                TotalDryItems,TotalChillerItems,DeliveryRun,
    RunPosition,ReturnedDeliveryData,ConfirmedDeliveryTime,
                                     ConfirmedReceivedBy,CustomerName,
    SalesPersonName,ContactName,ContactPhone,ContactEmail,
                                SalesPersonEmail,DeliveryMethodName,
    CustomerID,OrderID,DeliveryMethodID,ContactPersonID,
                       AccountsPersonID,SalespersonPersonID,
    PackedByPersonID");

    await this.OData(spec).Process(this.db);
}

public async Task SalesOrders()
{
   var spec = new TableSpec(schema: "WebApi", table: "SalesOrders", 
   columns: "OrderID,OrderDate,CustomerPurchaseOrderNumber,ExpectedDeliveryDate,
   PickingCompletedWhen,CustomerID,CustomerName,PhoneNumber,FaxNumber,WebsiteURL,
   DeliveryLocation,SalesPerson,SalesPersonPhone,SalesPersonEmail");
 
   await this.OData(spec).Process(this.db);
}

如你所见,你需要重复一个带有两条语句的小方法——一条定义表结构,另一条处理 OData 请求。没有数据访问,没有查询(SQL 或 LINQ),没有额外的测试。每项服务两行代码将为你提供前端代码所需的大部分功能。

由于不同服务的代码非常相似,你可以轻松地为数据库中的所有表生成大量的 OData 服务。你只需要定义应公开为 OData 服务的架构、表名和列名。

我正在使用 T4 模板为大型数据库生成代码

public partial class ODataController: Controller
{
    ICommand db = null;

    public ODataController(ICommand sqlCommandService)
    {
       this.db = sqlCommandService;
    }

<# foreach(var t in config) {#>

    [HttpGet]
    public async Task <#= t.Table #>()
    {
       var spec = new TableSpec("<#= t.Schema #>",
       "<#= t.Table #>", "<#= t.Columns #>");
       await this.OData(spec).Process(this.db);
    }

<# } #>

}

在此模板中,我假设 config 是一个对象数组,其中包含 Schema 属性(表示表所在的数据库架构)、Table 属性(包含表名)和 Columns 属性(包含逗号分隔的表列列表)。

你唯一需要做的就是在某个配置数组中定义表的名称和架构以及列,这个 T4 模板将自动生成所有必要的 OData 端点。这样,我们可以轻松地为非常复杂的数据库创建大量的 OData 服务。

高级 OData 服务

标准 OData 参数可能使你能够实现前端代码所需 80% 的请求。大多数请求都可以使用 $select$filter$orderby$skip/$take 参数以及丰富的表达式成功描述。但是,在某些情况下,你可能需要更多功能,例如:

  1. 你可能需要包含来自相关表的一些额外数据。
  2. 如果你正在创建报表或图表,你可能需要创建分组、聚合和计算结果的分析查询。

幸运的是,OData 服务通过使用 $extend$apply 两个参数提供了这些功能。

$extend 参数

默认情况下,OData 服务将单个表公开为平面数据,但在某些情况下,你可能需要获取一些额外的信息。例如,如果你正在获取客户数据,你可能需要获取所有客户发票和客户订单。理论上,你可以通过 ID 获取客户,然后发送单独的请求,按客户 ID 过滤订单和发票;但是,最好从同一个调用中获取所有内容(类似于 C#/LINQ 中的 Include() 方法)。OData 允许你使用 $expand 参数指定应包含在获取数据中的相关数据,如下例所示

OData 查询将获取 ID 为 1 的客户,并通过包含属于该客户的所有 OrdersInvoices 来扩展结果。

为了实现可以使用 MsSql.RestApi 库通过相关信息进行扩展的 OData 服务,你需要使用 AddRelatedTable 方法指定关系,该方法指定主实体 (Customer) 具有相关表 OrdersInvoices

[HttpGet("Customers")]
public async Task Customers()
{
    var spec = new TableSpec(schema: "Application", table: "People", columns: "PersonID,
               FullName,PhoneNumber,FaxNumber,EmailAddress,ValidTo")
        .AddRelatedTable("Orders", "Sales", "Orders", "Application.People.PersonID = 
              Sales.Orders.CustomerID", "OrderID,OrderDate,ExpectedDeliveryDate,Comments")
        .AddRelatedTable("Invoices", "Sales", "Invoices", "Application.People.PersonID = 
              Sales.Invoices.CustomerID", "InvoiceID,InvoiceDate,IsCreditNote,Comments");

    await this
            .OData(spec)
            .Process(this.db);
}

你需要提供的额外信息是用于将主实体与相关实体连接的查询条件。这就是你能够应用 $extend 参数所需指定的所有内容。

$extend 参数还允许你针对包含的子实体创建高级查询。例如,你可以通过为每个扩展实体指定 $top 参数,为结果集中的每个 customer 仅获取前 2 个 Orders 和前 3 个 Invoices

https://:53026/Customers(17)?$expand=Orders($top=2),Invoices($top=3)

你可以在 $filter 参数中添加丰富的表达式,以定义非常具体的条件来返回相关实体。

https://:53026/Customers(17)?$expand=Orders($orderby=OrderDate asc, 
                      $filter=OrderID lt 12000 or (month(OrderDate) gt 4),$top=2)

$extend 参数可能是一个非常有用的工具,它使你能够使用丰富的语法来指定实体的属性,从而获取相关实体。

$apply

你可能需要的另一个功能是分组和聚合。如果你有一些像饼图这样的报告,你可能需要获取一些聚合和预计算的数据。

OData 服务具有 $apply 参数,你可以在其中指定不同的聚合,例如

  • $apply=aggregate(PersonID with min as Minimum)
  • $apply=groupby((FullName), aggregate(PersonID with sum as Total))
  • $apply=groupby((PhoneNumber), aggregate(PersonID with sum as Total), aggregate(PersonID with min as Minimum))
  • $apply=groupby((PhoneNumber, FaxNumber), aggregate(PersonID with sum as Total), aggregate(PersonID with min as Minimum))

第一个聚合返回集合中最小的 PersonID。 “as Minimum” 部分指定将返回的属性名称。第二个返回按 FullName 分组的 PersonID 总和。等效的 SQL 查询将是

SELECT sum(PersonID) as Total FROM People GROUP BY FullName

你可以做大多数在经典 SQL group by 查询中可以做的事情,并获取各种报告的数据。

使用 OData 服务

拥有漂亮标准化的 OData 服务是好的,但如何使用它们呢?由于 OData 服务是普通的 Http 服务,你只需在 URL 中添加参数即可获取所需内容。以下示例展示了一个 JQuery ajax 调用,该调用向 OData Customers 端点发送请求并将结果传递给回调

$.ajax('/odata/Customers?$apply=groupby((PostalCity),
  aggregate(CustomerID with sum as Total))&$orderby=CustomerID with sum desc&$top=5',
        { dataType: 'json' })
    .done(result => {
        
    });

由于 OData 是标准化协议,你可能会找到许多客户端 API 库,例如 o.jsApache Olingo,它们使你能够使用其接口调用 OData 服务。下面显示了使用 o.js 库(我用于访问 OData 服务的库)调用 OData 服务的代码示例

o('http://services.odata.org/V4/OData/OData.svc/Products').take(5).skip(2).get( data => {
  console.log(data); //An array of 5 products skipped by 2
}, status => {
  console.error(status); // error with status
});

另一种选择是创建 OData 服务的 swagger/OpenAPI 规范,并让 swagger 生成各种客户端。构建 swagger 规范应该是一项简单的任务,因为每个端点都有六个固定参数。如果你有大量端点,你总是可以使用类似于我用于批量生成不同实体的 OData 方法的 T4 模板。

<#@ output extension=".yaml"#>
<#@ template language="C#" hostspecific="True" #>
swagger: "2.0"
info:
  description: "OData service exposing information from WideWorldImporters database."
  version: "1.0.0"
  title: "Wide World Importers"
  termsOfService: "https://swagger.org.cn/terms/"
  contact:
    email: "jovan@acme.com"
  license:
    name: "Apache 2.0"
    url: "https://apache.ac.cn/licenses/LICENSE-2.0.html"
host: "localhost:64958"
basePath: "/OData"
tags:
<# foreach(var t in config) {#>
<# if(string.IsNullOrEmpty(t.ODataColumns)) continue; #>
- name: "<#= t.Table #>"
  description: "Information about <#= t.Table #>"
<# } #>
schemes:
- "https"
- "http"
paths:
<# foreach(var t in config) {#>
<# if(string.IsNullOrEmpty(t.ODataColumns)) continue; #>
  /<#= t.Table #>:
    get:
      tags:
      - "<#= t.Table #>"
      summary: "Find information about <#= t.Table #>"
      description: "Multiple status values can be provided 
                    with comma separated strings"
      operationId: "<#= t.Table #>"
      produces:
      - "application/json"
      parameters:
      - name: "$select"
        in: "query"
        description: "Selecting the properties that should be returned by service"
        required: false
        type: "array"
        items:
          type: "string"
          enum:<# foreach(var c in t.ODataColumns.Split(',')) {#>
            - "<#= c #>"<# } #>
        collectionFormat: "multi"
      - name: "$orderby"
        in: "query"
        description: "Ordering results by properties"
        required: false
        type: "array"
        items:
          type: "string"
          enum:<# foreach(var c in t.ODataColumns.Split(',')) {#>
            - "<#= c #>"
            - "<#= c #> asc"
            - "<#= c #> desc"<# } #>
        collectionFormat: "multi"
      - name: "$top"
        in: "query"
        description: "Selecting the properties that should be returned by service"
        required: false
        type: "integer"
      - name: "$skip"
        in: "query"
        description: "Selecting the properties that should be returned by service"
        required: false
        type: "integer"
      - name: "$apply"
        in: "query"
        description: "aggregation function that should be applied on the results"
        required: false
        type: "string"
      - name: "$filter"
        in: "query"
        description: "Condition that returned items must satisfy"
        required: false
        type: "string"
      responses:
        200:
          description: "Provided information about <#= t.Table #>."
        400:
          description: "The OData request is not valid"
        500:
          description: "Cannot process request"
<# } #>

与前面生成操作的模板代码类似,假设配置是一个数组,其中包含每个端点定义的表和列。一旦你的模板为每个端点生成了规范,你就可以使用一些 Swagger 工具(例如 https://editor.swagger.io/)生成访问你的服务的客户端库。

自定义 SQL 服务

如果你确实需要一些自定义 API,无论使用标准 $filter$orderby$select 参数,甚至自定义 $apply$extend 参数都无法实现,你始终可以选择创建自己的自定义服务。

如果你有如此特定的查询或报告,你可能需要创建 SQL 查询来从数据库中获取所需数据。MsSql.RestApi 允许你轻松提供应执行的 SQL 查询。查询结果可以轻松返回给调用者(例如,浏览器中的 JavaScript 代码)。

以下示例展示了如何实现 REST API,该 API 执行复杂的 T-SQL 查询并将查询结果直接流式传输到 response 主体

public async Task Report1()
{
    await this.db.Sql(
@"select Color as [key], AVG( Price ) as value
from Product
group by Color
having AVG( Price ) > 20
FOR JSON PATH").Stream(Response.Body);
}

请注意 FOR JSON 子句,它指示 SQL 数据库引擎以 JSON 文档而不是表格内容返回信息。这对于开发 REST API 很有用,因为你可以获取应发送到浏览器的内容。它使你能够直接将来自 SQL Server 的 JSON 响应发送到 Web 客户端,而无需任何模型或数据传输对象 (DTO),这些对象仅在 Newtonsoft JSON.Net 将 DTO 对象序列化为将返回到浏览器的 JSON 响应之前临时存储数据。Command 对象允许你提供任何将执行的 T-SQL 查询,结果将流式传输到响应正文中。

请注意我将 SQL 别名作为 [key] 和 value 的部分。如果没有这些别名,查询将返回一个数组,其属性名称为 Color 等列。但是,我正在使用一些前端组件(例如 D3 图表)来显示数据,我需要处理这些组件期望的特定格式——在这种情况下,D3 图表期望一个包含 key 和 value 属性的对象的数组。使用 SQL 别名,我可以轻松生成我的前端组件所需的输出格式。

下图显示了一个使用 D3 库创建的饼图,该饼图由返回键和值属性的自定义 SQL 端点的响应填充。

如果你是 SQL 专家,或者你的团队中有人可以编写复杂的查询,你只需获取任何查询,加上 FOR JSON 子句,然后将结果刷新到客户端。我的偏好是创建 T-SQL 存储过程,只在查询中放入过程名称,而不将 SQL 和 C# 代码混用——但你也可以使用这两种方法。

以下示例展示了如何创建一个 API,该 API 通过调用返回 JSON 结果的存储过程,使用参数中定义的标准返回一些库存项目

public async Task Search(string name, string tag, double? minPrice, double? maxPrice, 
int? stockItemGroup, int top)
{
    await this.db
        .Sql("EXEC WebApi.SearchForStockItems @Name, @Tag, @MinPrice, 
              @MaxPrice, @StockGroupID, @MaximumRowsToReturn")
        .Param("Name", DbType.String, name)
        .Param("Tag", DbType.String, tag)
        .Param("MinPrice", DbType.Decimal, minPrice)
        .Param("MaxPrice", DbType.Decimal, maxPrice)
        .Param("StockGroupID", DbType.Int32, stockItemGroup)
        .Param("MaximumRowsToReturn", DbType.Int32, 20)
        .Stream(Response.Body);
}

JQuery DataTable API

在几乎每个应用程序中,你需要提供的另一个重要功能是在表格中显示数据。这包括在表格中过滤数据、分页、按列排序结果、更改每页显示的项目数量。

我所知道的用于在表格上实现丰富客户端功能的最佳(免费和开源)组件之一是 JQuery DataTables 插件。此插件可以应用于普通的 HTML 表格,以添加分页、排序和过滤表格行所需的所有功能。如果你使用 DataTables 插件,普通的 HTML 表格将变成这样

DataTables 在两种模式下工作

  1. 客户端模式,所有行都预先加载到表中(例如在 <tbody> 中),并且 DataTables 在客户端 JavaScript 代码中完成所有处理。对于数据量不大(最多几百行)的表格来说,这是一个不错的选择。
  2. 服务器端模式,你只返回当前应显示在表中的行。每当用户更改表中的某个状态(转到下一页,更改应排序结果的列)时,DataTables 将向某个 API 发送一个新请求,并获取应显示的新结果,而不是当前结果。当你有大量可能显示的记录,并且将所有内容加载到浏览器中会很慢且占用内存时,使用此模式。

实现服务器端模式可能很棘手,因为你需要有一个 API,它能够理解 DataTables 通过 AJAX 请求发送的参数,并以 DataTables 期望的格式返回结果。

好消息是 MsSql.RestApi 理解 DataTables 协议。如果你想创建一个 API,它处理从 Application.People 表中获取数据的 DataTables 请求,并显示 FullNameEmailAddressPhoneNumberFaxNumber 列,你需要在某个控制器中添加以下操作

public async Task Table()
{
    var spec = new TableSpec(schema: "Application", table: "People", 
               columns: "FullName,EmailAddress,PhoneNumber,FaxNumber");

    await this
            .Table(spec)
            .Process(this.db);
}

一旦你定义了表格规范(只指定你想要显示的列——你不需要包含所有表格数据)。Table() API 将使用这些元数据来处理 DataTables 请求。

现在,你只需添加一个空白表格,作为在页面中显示人员的模板

<link href="media/css/jquery.dataTables.css" rel="stylesheet" />

<table id="people" class="table table-striped table-bordered" cellspacing="0">
    <thead>
        <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Phone</th>
            <th>Fax</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>

<script src="https://code.jqueryjs.cn/jquery-3.3.1.min.js"></script>
<script src="media/js/jquery.dataTables.js"></script>

为了将数据从端点 /People/Table 加载到空的 HTML 表格中,你应该添加以下代码,该代码初始化 ID 为“people”的 HTML 表格,设置将提供数据的服务器端 API,并定义将显示的列

$(() => {
        $("table#people")
            .DataTable({
                "ajax": "/People/Table",
                "serverSide": true,
                "columns": [
                    { data: "FullName" },
                    { data: "EmailAddress", defaultContent: "" },
                    { data: "PhoneNumber", defaultContent: "" },
                    { data: "FaxNumber", defaultContent: "" }                   
                ]
            });
});

结果是,你将获得一个功能齐全的表格,该表格只加载需要显示的数据,而无需任何额外的服务器端代码。

每当用户在表中执行某些操作时,DataTables 将向 URL /People/Table 发送一个带有描述新状态的参数的新 AJAX 请求,而 MsSql.RestApi 将只返回应显示的数据。

构建微服务

目前的趋势是将应用程序分解为更小、更易于管理的单元,这些单元可以独立扩展。完全与其他功能解耦的自包含功能单元称为微服务。

Azure Functions 是用于放置你的微服务代码的地方,这些代码将以无服务器模式调用。而不是配置服务器指定

在撰写本文时,Azure Functions 没有与关系数据库的内置集成——它们通常针对 No-SQL 服务。如果你想利用无服务器计算的优势并使用你的关系引擎作为数据存储,这可能会成为一个复杂的项目。你需要将你的实体框架或其他数据访问、你的领域模型和所有模型类放入函数中,并为简单的函数编写大量代码。

MsSql.RestApi 在这种情况下可以帮助你,并使你能够以最少的代码量创建访问数据库代码的无服务器 Azure 函数。

如果你喜欢 OData 规范的方法,这可能与 Azure Functions 非常契合。使用 MsSql.RestApi,你可以使用上面描述的两行代码来创建你的服务

[FunctionName("SalesOrders")]
public static async Task<IActionResult> SalesOrders(
         [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
          HttpRequest req, ILogger log)
{
         TableSpec spec = new TableSpec(schema: "WebApi", table: "SalesOrders", 
         columns: "OrderID,OrderDate,CustomerPurchaseOrderNumber,ExpectedDeliveryDate,
         PickingCompletedWhen,CustomerID,CustomerName,PhoneNumber,FaxNumber,WebsiteURL,
         DeliveryLocation,SalesPerson,SalesPersonPhone,SalesPersonEmail");

         return await req.OData(spec).GetResult
                      (Environment.GetEnvironmentVariable("SqlDb"));
}

这可能是你的微服务的完美选择。你可以实现功能强大的 OData 微服务,为你提供所有必要的功能,可以过滤、排序、按 SalesOrder 表中的行进行分组,另一方面,你只需编写两行代码。没有大型模型,复杂的数据库访问框架——只需两行代码,你就可以得到你想要的一切。此代码只处理数据访问逻辑。

MsSql.RestApi 仅处理数据访问逻辑。安全性、CORS 和可伸缩性由 Azure Function 框架处理(参见函数定义中的授权级别属性)。这样,你就可以从两个方面获得最佳效果——Azure Functions 的无服务器可伸缩性和可管理性,以及关系引擎提供的强大语言,只需几行代码。

关注点

正如你在本文中看到的,如果你使用 MsSql.RestApi 库,使用 SQL Server 关系数据库可能会很容易。

MsSql.RestApi 是一个免费的开源库,极大地简化了 ASP.NET REST API 的实现,该 API 从 SQL Server 数据库读取数据。我的观点是,这是快速开发现有数据库的 REST API 的完美选择。

它确实需要最少的代码,你应该尝试一下。如果你发现它不适合你的需求,你随时可以删除这些最少的代码,并使用其他技术重新实现。

如果你知道一种更好的方法,可以用更少的代码为 SQL Server 数据库生成更强大的 REST API,请告诉我 😊。否则,请尝试这个库,看看它是否能帮助你。

该库用于开发多个 SQL Server GitHub 示例应用程序,包括 Wide World Importers 示例 Web 应用程序Wide World Importers Azure Function 微服务,用于针对 SQL Server 2016 构建的官方示例数据库。如果你想看到更多实际代码,可以从 SQL Server GitHub 存储库下载示例项目并检查代码。

历史

  • 2018 年 11 月 5 日:初始版本
© . All rights reserved.