如何使用 C# 和 .NET 创建无服务器 API
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
在本文中,我们将学习如何使用 C# 和 .NET 构建一个无服务器函数。我们将解释无服务器的原理,并学习如何在 VS Code 中构建、运行和调试我们的第一个函数。
如果您对如何在 JavaScript 中编写函数感兴趣,请看下文,否则,请继续阅读。
如果您对 JavaScript 和无服务器感兴趣,请查看本系列
在我们开始构建第一个函数之前,让我们先提一下我们将在本文中介绍的内容:
- 为何选择无服务器?问问自己为什么要做这件事至关重要。有一些标准使使用无服务器函数成为一个不错的选择。
- 核心概念:无服务器架构包含一系列概念,你需要了解这些概念才能高效地使用它,并大致了解它的基本原理。你需要了解的最重要的概念是输入/输出绑定。
- 构建我们的第一个函数,我们将在这里介绍如何通过设置触发器来编写函数,以及如何解析来自路由/查询参数或已发布的 Body 的输入。我们还将了解如何定义输出。
资源
-
注册免费的 Azure 帐户
要创建无服务器 Azure 函数,您需要一个免费的 Azure 帐户 -
在 VSCode 中创建您的第一个 Azure 函数
一篇很棒的博客文章,将向您展示函数的创建以及部署 -
使用 C# 实现 Azure Functions
Azure Functions 的 AC# 参考 -
安装 Azure Functions Core Tools
您将需要 Azure Functions Core Tools 才能从 VS Code 运行和调试 Azure Functions -
Azure Functions 文档概述
您还可以使用 Azure CLI 和门户等创建 Azure Functions -
Azure CLI 安装
Azure CLI 是一种从终端管理云资源的绝佳方式,通常比使用门户快得多。 -
https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings
所有触发器、输入和输出绑定的完整列表
为什么无服务器
Serverless 是一种云计算执行模型。
好吧,听起来有点棘手。我们到底想说什么呢?
很简单,我们编写的函数将在云端执行。这意味着我们无需关心分配虚拟机,甚至无需运行 Web 服务器来运行代码,这些都由云端自行管理。这也意味着资源分配也由云端自行管理。
听起来不错,还有什么?
选择无服务器的主要原因之一是成本或零成本。无服务器很便宜。通常情况下,您只需在函数实际运行时付费。
怎么会便宜呢?
您的函数并非始终存在,它们的实例会在您需要时启动。这意味着您可能会遇到所谓的冷启动。这意味着实例首次启动并开始与其交互可能需要一些时间。
我不确定这听起来是否很好,如果我希望我的客户能够全天候使用我的功能,或者至少避免这种冷启动,那该怎么办?
如果你真的想避免冷启动,有一个高级计划
https://docs.microsoft.com/en-us/azure/azure-functions/functions-premium-plan
但在你开始之前,让我们先讨论一下在那里实际运行什么代码。
好的?
是的,所有类型的代码和业务逻辑都不应该在那里运行。只有那些你很少用到的东西才应该以无服务器的方式运行,比如一些实用函数或某种计算。
好的,我明白了。我想我的代码库里有一些代码可以转换成无服务器模式,不需要那么频繁地运行。这样做可以省钱,对吧?
是的,很少运行的代码可以变成无服务器的,从而可以节省大量的钱。
就这些吗?
嗯,不止于此,无服务器通常是更大图景的一部分,因为它可以与大多数云资源交互。它可以被从数据库到队列到 HTTP 的一切触发,我们也可以轻松地将计算结果传递给云中的另一个服务。
哇,听起来......真的很棒。
核心概念
好的,我们已经提到了部分核心概念,但让我们更清楚地了解这些概念是什么。
我们得到以下概念:
- 触发器,用于触发函数启动。触发器可以是 HTTP 调用、触发事件、数据库中的一行数据等等。
- 输入绑定,输入绑定是与资源的连接,可以是 SignalR、表存储、CosmosDB 等等。关键在于我们无需实例化连接,它已经预先创建好了,随时可以进行交互。
- 输出绑定,就是你可以写入的内容。例如,你可以使用输入绑定连接到数据库并读取产品列表。相反,你可以使用输出绑定,根据函数的输入数据创建新的产品条目。
这可能听起来有点奇怪,但当我们学会创建一些函数时就会变得更加清晰。
我建议看一下这个链接
https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings
查看所有受支持的输入/输出绑定的完整范围
构建我们的第一个函数
好的,我们来到了最后的部分,编写一些函数。在开始之前,我们需要安装一些扩展程序来提升我们的创作体验。
设置
我们需要以下材料:
- Azure Functions 核心工具
- 适用于 VS Code 的 Azure Functions 扩展,搜索
Azure Functions
。它应该如下所示:
- 因为我们将使用 C#,所以我们需要它的扩展,它应该看起来像这样
让我们开始编码
我们已经准备好编写代码了。我们将执行以下操作:
- 生成 Azure 函数应用
- 创建由 HTTP 触发的 Azure 函数
生成 Azure Function App
我们的函数需要托管在应用程序中。一个应用程序可以包含多个函数。这很简单,只需在 VS Code 中通过选择View/Command Palette
或按下组合键(COMMAND + SHIFT + P
如果您使用的是 Mac)打开命令面板即可。打开菜单后,输入:Azure Functions: Create new project
。
这将要求您提供:
- 目录,将应用程序存储在哪个目录中,选择当前目录
- 代码语言,它会继续询问你代码语言,选择C#
- 触发器,然后它会询问你的第一个函数触发器,选择 HTTP 触发器
- 函数名称,之后它会要求输入函数名称,输入
Hello
- 命名空间,然后会要求您提供一个命名空间,现在,使用
Company.Function
,您以后可以随时更改它 - 访问权限,当被问及访问权限时,请选择
Anonymous
。安全性很重要,但在本练习中,我们将重点了解各个部分,以后可以加强安全性。
它还会询问您第一个函数的名称、语言和触发器类型。
这将为您生成一个项目。但是,一开始它会用红色字体提示存在未解决的依赖项。您可以通过点击restore
项目生成完成后立即出现的弹出窗口来解决这个问题,或者在终端中,在项目根目录下输入以下命令:
点网恢复
这将恢复项目中指出的依赖关系,您很快就会看到代码再次看起来令人满意。
函数的剖析
让我们看一下Hello.cs
创建应用程序时生成的文件:
// Hello.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Company.Function
{
public static class Hello
{
[FunctionName("Hello")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
}
我们需要了解的关于用 C# 编写的 Azure Functions 的大部分内容都在这段代码中。让我们从头开始看一下:
- FunctionName,这是一个接受字符串参数的装饰器。当我们调试或以其他方式测试应用程序时,该函数将被调用。使用字符串参数是
Hello
为了确保该函数的最终 URL 是[someUrl]/Hello
。 - 运行,这是一个任意函数名,可以叫任何名字
- HttpTrigger,这是一个装饰器,用于装饰我们的 HttpRequest 参数
req
。我们从装饰器开始,将 设置AuthorizationLevel
为Anonymous
,这意味着任何外部人员都可以访问该函数。还有其他选项,AuthorizationLevel
我们将在以后的文章中探讨。 - HTTP 动词列表
get
,接下来是一个由 HTTP 动词组成的参数字符串列表。我们可以看到,到目前为止,我们已经有了值post
,这意味着该函数将响应这些 HTTP 动词。我们当然可以扩展列表,"get", "post", "put", "delete"
但在 REST 中,每种操作类型通常只支持一个动词。 - 路由模式,下一个值为
Route
。如果我们更改此值,则可以开始支持路由参数,稍后我们将展示这一点。
这就是 HttpRequest 参数的全部内容。让我们看看log
类型ILogger
。通过调用此参数,我们可以写入日志。日志在调试时会在本地可见,但在部署应用后也会在门户中可见。
好的,那么我们来看看生成的函数体:
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
第一行告诉我们如何解析查询参数。我们使用Query
带有字符串参数的 来实现。例如,如果您的 URL 如下所示,则url/Hello?name='some value'
变量name
将保存值some value
。
接下来,我们使用 StreamReader 读取 Body,注意关键字 的使用await
。注意,函数头开头的 this 是如何与async
关键字匹配的。接下来的一行是调用JsonConvert.DeserializeObject
并将其赋值给dynamic
。在下一行,我们尝试读取name
Body,前提是 Body 非空。最后,我们使用或 来返回200
响应。OkObjectResult
401
BadRequestObjectResult
运行函数
接下来,我们将使用 VS Code 调试运行该函数,并尝试 GET 和 POST 操作。转到菜单并选择Debug /Start Debugging
“这将编译您的应用程序并启动运行时”。完成后,您应该会看到以下内容:
转到浏览器并输入指示的 URL http://localhost:7071/api/hello
:
这段代码显然不尽如人意,因为我们没有提供查询参数,也没有提交数据主体。我们试试添加查询参数?name=chris
,http://localhost:7071/api/hello?name=chris
输入以下内容:
一切都好!
让我们设置一个断点,然后重新加载浏览器:
正如您在上面看到的,断点被击中,我们能够像往常一样检查和调试我们的代码,很好:)
我们已经成功实现了 GET 调用,那么 POST 调用呢?打开你常用的 REST 客户端,cURL 还是 Advanced Rest Client,你自己决定。然后创建一个如下请求:
然后查看断点是如何被触发的,如下所示:
构建 REST API
我们想做的不仅仅是一个简单的函数,也就是生成一个 REST API。我们不会使用数据库,而是使用一个保存状态的静态类。
为此创建一个全新的目录,并创建一个新的 Azure Function App。当系统询问方法名称时,请ProductsGet
选择HttpTrigger
然后使用命令面板创建以下方法:
- 产品创建
- 产品获取
为了生成这些方法,我们打开命令面板,这次我们选择Azure Functions: Create Function
。选择HttpTrigger
并命名。此时,您的项目应该如下所示:
现在怎么办?
我们将按顺序介绍每个生成的类,但首先让我们创建保存数据库的类Db.cs
创建数据库
这只是一个保存我们数据的简单类,但在以后的文章中,我们将用实际的数据库取代它。
// Db.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace Company {
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
public static class Db {
private static List<Product> products = new List<Product>(){
new Product(){ Id = 1, Name= "Avengers End Game" },
new Product(){ Id = 2, Name= "Wonder Woman" }
};
public static IEnumerable<Product> GetProducts()
{
return products.AsEnumerable();
}
public static Product GetProductById(int id)
{
return products.Find(p => p.Id == id);
}
public static Product CreateProduct(string name)
{
var newProduct = new Product(){ Id = products.Count + 1, Name = name};
products.Add(newProduct);
return newProduct;
}
}
}
正如您在上面看到的,我们创建了类型Product
和方法GetProducts
,GetProductById()
并且CreateProduct()
。
产品列表
这将返回产品列表并且看起来非常简单:
// ProductsGet.cs
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Company.Function
{
public static class ProductsGet
{
[FunctionName("ProductsGet")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
var json = JsonConvert.SerializeObject(new{
products = Db.GetProducts()
});
return (ActionResult)new OkObjectResult(json);
}
}
}
值得评论一下我们如何创建响应:
var json = JsonConvert.SerializeObject(new{
products = Db.GetProducts()
});
我们在这里要做的是创建一个如下所示的 JSON 响应:
{
"products": [/* list of products*/]
}
还值得注意的是我们如何将允许的 HTTP 动词限制为仅有get
。
获取特定产品
现在,这是关于查询特定产品的。我们将通过创建类似这样的路由来实现url/ProductGet/{productid}
。我们的想法是,通过输入例如来查找特定产品url/ProductGet/1
。让我们看一下实现:
// ProductGet.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Company.Function
{
public static class ProductGet
{
[FunctionName("ProductGet")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "ProductsGet/{id:int}")] HttpRequest req,
int id,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
var product = Db.GetProductById(id);
var json = JsonConvert.SerializeObject(new {
product = product
});
return (ActionResult) new OkObjectResult(json);
}
}
}
特别注意我们在HttpTrigger
装饰器中如何将 的值设置Route
为ProductsGet/{id:int}
。这是一种模式,意味着我们将响应类似 的请求。我们只需输入 ,就能从路由中url/ProductsGet/1
解析出其他内容。然后,我们只需通过以下代码筛选出匹配的产品,并创建 JSON 响应:id
int id
var product = Db.GetProductById(id);
var json = JsonConvert.SerializeObject(new {
product = product
});
创建产品
在本例中,我们将 HTTP 动词限制为post
。我们还将从正文中读取并解析出name
。代码如下所示:
// ProductCreate.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Company.Function
{
public static class ProductCreate
{
[FunctionName("ProductCreate")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = null;
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
log.LogInformation("request body", requestBody);
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
if(name != null)
{
log.LogInformation("name", name);
var product = Db.CreateProduct(name);
var json = JsonConvert.SerializeObject(new{
product = product
});
return (ActionResult)new OkObjectResult(json);
}
else {
return new BadRequestObjectResult("Missing name in posted Body");
}
}
}
}
这里有趣的部分是我们如何使用以下代码解析出 Body 信息:
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
log.LogInformation("request body", requestBody);
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
除此之外,代码非常简单,并检查name
Body 中是否提供了,如果是,则继续创建它,最后创建 JSON 响应。
概括
好的,我们取得了一些进展。我们理解了选择无服务器的原因,并能够创建两个不同的 Azure Function 应用。一个比较简单,展示了处理查询参数和主体等概念;另一个示例展示了一个接近生产代码的 REST API。
这是第一篇关于无服务器和 C# 的文章,希望您现在已经掌握了基础知识。我们只是触及了皮毛。真正的价值在于无服务器框架能够设置不同的触发器、输入和输出绑定,并与 Azure 上的许多其他服务进行交互。
文章来源:https://dev.to/azure/how-you-can-create-a-serverless-api-in-c-and-net-1ie