你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

有关在独立工作进程中运行 C# Azure Functions 的指南

本文介绍如何使用 .NET Functions 独立工作进程,它在 Azure 的独立工作进程中运行函数。 这使你可以在与 Functions 主机进程所用版本不同的 .NET 版本上运行 .NET 类库函数。 有关支持的特定 .NET 版本的信息,请参阅支持的版本

使用以下链接立即开始构建 .NET 独立工作进程函数。

入门 概念 示例

如果仍需在主机所用的同一进程中运行函数,请参阅进程内 C# 类库函数

有关独立工作进程与进程内 .NET Functions 之间的全面比较,请参阅进程内和独立工作进程 .NET Azure Functions 之间的差异

为何使用 .NET Functions 隔离工作进程?

引入时,Azure Functions 仅支持 .NET 函数的紧密集成模式。 在此进程内模式下,.NET 类库函数在主机所用的同一进程中运行。 此模式在主机进程与函数之间提供深度集成。 例如,在同一进程中运行时,.NET 类库函数可以共享绑定 API 和类型。 但是,这种集成还要求在主机进程与 .NET 函数之间实现紧密耦合。 例如,在进程内运行的 .NET 函数需要在 Functions 运行时所用的同一 .NET 版本上运行。 这意味着进程内函数只能在具有长期支持 (LTS) 的 .NET 版本上运行。 为了能够在非 LTS 版本的 .NET 上运行,可以选择在独立工作进程中运行。 借助这种进程隔离,还可以开发使用不受 Functions 运行时原生支持的当前 .NET 版本(包括 .NET Framework)的函数。 独立工作进程和进程内 C# 类库函数都在 LTS 版本上运行。 若要了解详细信息,请参阅支持的版本

由于这些函数在独立的进程中运行,.NET 隔离的函数应用与 .NET 类库函数应用之间存在一些特性和功能差异

独立工作进程的优点

当 .NET 函数在独立工作进程中运行时,你可以利用以下优势:

  • 减少冲突:由于函数在独立的进程中运行,因此应用中使用的程序集不会与主机进程所用的相同程序集的不同版本发生冲突。
  • 全面控制进程:可以控制应用的启动,并可控制所用的配置和启动的中间件。
  • 依赖项注入:由于可以全面控制进程,因此可以使用当前的 .NET 依赖项注入行为,并将中间件整合到函数应用中。

支持的版本

Functions 运行时版本使用特定版本的 .NET。 若要详细了解 Functions 版本,请参阅 Azure Functions 运行时版本概述。 版本支持取决于 Functions 是在进程内运行还是在独立工作进程中运行。

注意

若要了解如何更改函数应用使用的 Functions 运行时版本,请参阅查看和更新当前运行时版本

下表显示了可与特定版本的 Functions 配合使用的 .NET Core 或 .NET Framework 的最高级别。

Functions 运行时版本 进程内
.NET 类库
独立工作进程
.NET 独立
Functions 4.x .NET 6.0 .NET 6.0
.NET 7.0 (GA)1
.NET Framework 4.8 (GA)1
Functions 3.x .NET Core 3.1
Functions 2.x .NET Core 2.12 不适用
Functions 1.x .NET Framework 4.8 不适用

1 生成过程还需要使用 .NET 6 SDK。 对 .NET Framework 4.8 的支持目前为正式版。

2 有关详细信息,请参阅 Functions v2.x 注意事项

有关 Azure Functions 版本的最新消息,包括删除较旧的特定次要版本,请关注 Azure 应用服务公告

.NET 独立工作进程项目

简单而言,.NET 隔离函数项目是一个 .NET 控制台应用项目,它面向受支持的 .NET 运行时。 下面是任何 .NET 隔离项目中所需的基本文件:

有关完整示例,请参阅 .NET 6 独立示例项目.NET Framework 4.8 独立示例项目

注意

若要将独立函数项目发布到 Azure 中的 Windows 或 Linux 函数应用,必须在远程 FUNCTIONS_WORKER_RUNTIME 应用程序设置中设置值“dotnet-isolated”。 若要在 Linux 上支持 zip 部署以及从部署包运行,还需要将 linuxFxVersion 站点配置设置更新为 DOTNET-ISOLATED|7.0。 若要了解详情,请参阅 Linux 上的手动版本更新

包引用

.NET Functions 独立工作进程项目对核心功能和绑定扩展都使用一组唯一的包。

核心包

需要使用以下包在独立工作进程中运行 .NET 函数:

扩展包

由于 .NET 独立工作进程函数使用不同的绑定类型,因此它们需要一组唯一的绑定扩展包。

可以在 Microsoft.Azure.Functions.Worker.Extensions 下找到这些扩展包。

启动和配置

使用 .NET 隔离函数时,可以访问函数应用的启动代码(通常在 Program.cs 中)。 你需要负责创建并启动自己的主机实例。 因此,你还可以直接访问应用的配置管道。 使用 .NET Functions 独立工作进程时,可以更轻松地添加配置、注入依赖项并运行自己的中间件。

以下代码显示了 HostBuilder 管道的示例:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(builder =>
    {
        builder
            .AddApplicationInsights()
            .AddApplicationInsightsLogger();
    })
    .ConfigureServices(s =>
    {
        s.AddSingleton<IHttpResponderService, DefaultHttpResponderService>();
    })
    .Build();

此代码需要 using Microsoft.Extensions.DependencyInjection;

HostBuilder 用于生成并返回已完全初始化的 IHost 实例,该实例以异步方式运行以启动函数应用。

await host.RunAsync();

重要

如果项目面向 .NET Framework 4.8,则还需要在创建 HostBuilder 之前添加 FunctionsDebugger.Enable();。 它应该是方法 Main() 的第一行。 有关详细信息,请参阅以 .NET Framework 为目标时进行调试

Configuration

ConfigureFunctionsWorkerDefaults 方法用于添加函数应用在独立工作进程中运行时所需的设置,其中包括以下功能:

  • 转换器的默认设置。
  • 设置默认 JsonSerializerOptions 以忽略属性名称大小写。
  • 与 Azure Functions 日志记录集成。
  • 输出绑定中间件和功能。
  • 函数执行中间件。
  • 默认的 gRPC 支持。
.ConfigureFunctionsWorkerDefaults(builder =>
{
    builder
        .AddApplicationInsights()
        .AddApplicationInsightsLogger();
})

能够访问主机生成器管道意味着还可以在初始化期间设置任何特定于应用的配置。 可以一次或多次调用HostBuilder 中的 ConfigureAppConfiguration 方法来添加函数应用所需的配置。 若要详细了解应用配置,请参阅 ASP.NET Core 中的配置

这些配置将应用到在独立进程中运行的函数应用。 若要更改 Functions 主机或触发器以及绑定配置,仍需使用 host.json 文件

依赖关系注入

与 .NET 类库相比,依赖项注入已得到简化。 不必创建启动类来注册服务,而只需在主机生成器中调用 ConfigureServices,并使用 IServiceCollection 中的扩展方法来注入特定的服务。

以下示例注入单一实例服务依赖项:

.ConfigureServices(s =>
{
    s.AddSingleton<IHttpResponderService, DefaultHttpResponderService>();
})

此代码需要 using Microsoft.Extensions.DependencyInjection;。 有关详细信息,请参阅 ASP.NET Core 中的依赖项注入

中间件

.NET 隔离进程还支持中间件注册,这同样是使用与 ASP.NET 中类似的模型实现的。 使用此模型可以在执行函数之前和之后将逻辑注入到调用管道中。

ConfigureFunctionsWorkerDefaults 扩展方法具有一个重载,可让你注册自己的中间件,如以下示例中所示。

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApplication =>
    {
        // Register our custom middlewares with the worker

        workerApplication.UseMiddleware<ExceptionHandlingMiddleware>();

        workerApplication.UseMiddleware<MyCustomMiddleware>();

        workerApplication.UseWhen<StampHttpHeaderMiddleware>((context) =>
        {
            // We want to use this middleware only for http trigger invocations.
            return context.FunctionDefinition.InputBindings.Values
                          .First(a => a.Type.EndsWith("Trigger")).Type == "httpTrigger";
        });
    })
    .Build();

UseWhen 扩展方法可用于注册按条件执行的中间件。 必须将一个返回布尔值的谓词传递给此方法,如果该谓词的返回值为 true,则中间件将参与调用处理管道。

利用 FunctionContext 上的以下扩展方法可在独立模型中更轻松地使用中间件。

方法 说明
GetHttpRequestDataAsync 在被 HTTP 触发器调用时获取 HttpRequestData 实例。 此方法返回 ValueTask<HttpRequestData?> 的实例,当你要读取消息数据(例如请求标头和 Cookie)时,该实例很有用。
GetHttpResponseData 在被 HTTP 触发器调用时获取 HttpResponseData 实例。
GetInvocationResult 获取 InvocationResult 的实例,该实例表示当前函数执行的结果。 根据需要使用 Value 属性获取或设置值。
GetOutputBindings 获取当前函数执行的输出绑定条目。 此方法结果中的每个条目的类型都是 OutputBindingData。 可以根据需要使用 Value 属性获取或设置值。
BindInputAsync 为请求的 BindingMetadata 实例绑定一个输入绑定项。 例如,当函数的 BlobInput 输入绑定需要由中间件访问或更新时,可以使用此方法。

下面是一个中间件实现示例,它在函数执行期间读取 HttpRequestData 实例并更新 HttpResponseData 实例。 此中间件检查是否存在特定请求标头 (x-correlationId),如果存在,则此中间件将使用该标头值标记响应标头。 否则,它会生成一个新的 GUID 值,并使用该值来标记响应标头。

internal sealed class StampHttpHeaderMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var requestData = await context.GetHttpRequestDataAsync();

        string correlationId;
        if (requestData!.Headers.TryGetValues("x-correlationId", out var values))
        {
            correlationId = values.First();
        }
        else
        {
            correlationId = Guid.NewGuid().ToString();
        }

        await next(context);

        context.GetHttpResponseData()?.Headers.Add("x-correlationId", correlationId);
    }
}

有关在函数应用中使用自定义中间件的更完整示例,请参阅自定义中间件参考示例

取消令牌

函数可以接受 CancellationToken 参数,以使操作系统能够在函数即将终止时通知代码。 可以使用此通知来确保该函数不会意外终止,导致数据处于不一致状态。

取消令牌在独立工作进程中运行时,受 .NET 函数支持。 以下是在收到取消请求时引发异常的示例:

[Function(nameof(ThrowOnCancellation))]
public async Task ThrowOnCancellation(
    [EventHubTrigger("sample-workitem-1", Connection = "EventHubConnection")] string[] messages,
    FunctionContext context,
    CancellationToken cancellationToken)
{
    _logger.LogInformation("C# EventHub {functionName} trigger function processing a request.", nameof(ThrowOnCancellation));

    foreach (var message in messages)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await Task.Delay(6000); // task delay to simulate message processing
        _logger.LogInformation("Message '{msg}' was processed.", message);
    }
}

以下是在收到取消请求时执行清理操作的示例:

[Function(nameof(HandleCancellationCleanup))]
public async Task HandleCancellationCleanup(
    [EventHubTrigger("sample-workitem-2", Connection = "EventHubConnection")] string[] messages,
    FunctionContext context,
    CancellationToken cancellationToken)
{
    _logger.LogInformation("C# EventHub {functionName} trigger function processing a request.", nameof(HandleCancellationCleanup));

    foreach (var message in messages)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            _logger.LogInformation("A cancellation token was received, taking precautionary actions.");
            // Take precautions like noting how far along you are with processing the batch
            _logger.LogInformation("Precautionary activities complete.");
            break;
        }

        await Task.Delay(6000); // task delay to simulate message processing
        _logger.LogInformation("Message '{msg}' was processed.", message);
    }
}

ReadyToRun

可以将函数应用编译为 ReadyToRun 二进制文件。 ReadyToRun 是一种预先编译形式,可以提高启动性能,帮助降低在消耗计划中运行时的冷启动的影响。

ReadyToRun 在 .NET 3.1、.NET 6(进程内和独立工作进程)和 .NET 7 中可用,并且需要 Azure Functions 运行时的版本 3.0 或更高版本

若要将项目编译为 ReadyToRun,请通过添加 <PublishReadyToRun><RuntimeIdentifier> 元素来更新项目文件。 以下是用于发布到 Windows 32 位函数应用的配置。

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  <RuntimeIdentifier>win-x86</RuntimeIdentifier>
  <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>

执行上下文

.NET 隔离进程将 FunctionContext 对象传递给函数方法。 使用此对象可以通过调用 GetLogger 方法并提供 categoryName 字符串,来获取要写入到日志的 ILogger 实例。 有关详细信息,请参阅日志记录

绑定

绑定是通过在方法、参数和返回类型中使用特性定义的。 函数方法是包含 Function 特性以及一个应用于输入参数的触发器特性的方法,如以下示例中所示:

[Function("QueueFunction")]
[QueueOutput("output-queue")]
public static string[] Run([QueueTrigger("input-queue")] Book myQueueItem, FunctionContext context)

触发器属性指定触发器类型并将输入数据绑定到一个方法参数。 以上示例函数将由一条队列消息触发,队列消息将传递给方法中的 myQueueItem 参数。

Function 属性将该方法标记为函数入口点。 该名称在项目中必须是唯一的,以字母开头,并且只包含字母、数字、_-,长度不得超过 127 个字符。 项目模板通常创建一个名为 Run 的方法,但方法名称可以是任何有效的 C# 方法名称。

由于 .NET 隔离项目在独立的工作进程中运行,因此绑定无法利用丰富的绑定类,例如 ICollector<T>IAsyncCollector<T>CloudBlockBlob。 此外,不直接支持继承自底层服务 SDK 的类型,例如 DocumentClientBrokeredMessage。 绑定依赖于字符串、数组和可序列化类型,例如普通旧类对象 (POCO)。

对于 HTTP 触发器,必须使用 HttpRequestDataHttpResponseData 来访问请求与响应数据。 这是因为,使用 .NET Functions 独立工作进程时,无法访问原始 HTTP 请求和响应对象。

有关将触发器和绑定与独立工作进程函数一起使用的一组完整参考示例,请参阅绑定扩展参考示例

输入绑定

一个函数可以有零个或多个可向函数传递数据的输入绑定。 与触发器一样,输入绑定是通过向输入参数应用绑定特性来定义的。 执行函数时,运行时将尝试获取绑定中指定的数据。 请求的数据通常依赖于触发器使用绑定参数提供的信息。

输出绑定

若要写入到输出绑定,必须将输出绑定特性应用到函数方法,该方法定义了如何写入到绑定的服务。 该方法返回的值将写入到输出绑定。 例如,以下示例使用输出绑定将一个字符串值写入到名为 myqueue-output 的消息队列:

[Function("QueueFunction")]
[QueueOutput("output-queue")]
public static string[] Run([QueueTrigger("input-queue")] Book myQueueItem, FunctionContext context)
{
    // Use a string array to return more than one message.
    string[] messages = {
        $"Book name = {myQueueItem.Name}",
        $"Book ID = {myQueueItem.Id}"};
    var logger = context.GetLogger("QueueFunction");
    logger.LogInformation($"{messages[0]},{messages[1]}");

    // Queue Output messages
    return messages;
}

多个输出绑定

写入到输出绑定的数据始终是函数的返回值。 如果需要写入到多个输出绑定,必须创建自定义返回类型。 在此返回类型中,必须已将输出绑定特性应用到类的一个或多个属性。 以下来自 HTTP 触发器的示例写入到 HTTP 响应和队列输出绑定:

public static class MultiOutput
{
    [Function("MultiOutput")]
    public static MyOutputType Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req,
        FunctionContext context)
    {
        var response = req.CreateResponse(HttpStatusCode.OK);
        response.WriteString("Success!");

        string myQueueOutput = "some output";

        return new MyOutputType()
        {
            Name = myQueueOutput,
            HttpResponse = response
        };
    }
}

public class MyOutputType
{
    [QueueOutput("myQueue")]
    public string Name { get; set; }

    public HttpResponseData HttpResponse { get; set; }
}

来自 HTTP 触发器的响应始终被视为输出,因此不需要返回值属性。

HTTP 触发器

HTTP 触发器将传入的 HTTP 请求消息转换为要传递给函数的 HttpRequestData 对象。 此对象提供请求中的数据,包括 HeadersCookiesIdentitiesURL 和可选消息 Body。 此对象是 HTTP 请求对象的表示形式,而不是请求本身。

同样,函数返回一个 HttpReponseData 对象,该对象提供用于创建 HTTP 响应的数据,包括消息 StatusCodeHeaders 和可选消息 Body

以下代码是一个 HTTP 触发器

[Function("HttpFunction")]
public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req,
    FunctionContext executionContext)
{
    var logger = executionContext.GetLogger("HttpFunction");
    logger.LogInformation("message logged");

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Date", "Mon, 18 Jul 2016 16:06:00 GMT");
    response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

    response.WriteString("Welcome to .NET 5!!");

    return response;
}

日志记录

在 .NET 隔离进程中,可以使用 ILogger 实例写入日志,该实例是从传递给函数的 FunctionContext 对象获取的。 调用 GetLogger 方法并传递一个字符串值,该值是在其中写入日志的类别的名称。 该类别通常是从中写入日志的特定函数的名称。 若要详细了解类别,请参阅有关监视的文章

以下示例演示如何获取 ILogger 并在函数中写入日志:

var logger = executionContext.GetLogger("HttpFunction");
logger.LogInformation("message logged");

使用 ILogger 的各种方法写入各种日志级别,例如 LogWarningLogError。 若要详细了解日志级别,请参阅有关监视的文章

使用依赖项注入时还会提供 ILogger

面向 .NET Framework 时的调试

如果独立项目面向 .NET Framework 4.8,则当前预览版范围需要你执行手动步骤才能启用调试。 如果使用其他目标框架,则不需要执行这些步骤。

应用应首先调用 FunctionsDebugger.Enable(); 作为其第一个操作。 在 Main() 方法中初始化 HostBuilder 之前会发生此操作。 Program.cs 文件的内容应类似于:

using System;
using System.Diagnostics;
using Microsoft.Extensions.Hosting;
using Microsoft.Azure.Functions.Worker;
using NetFxWorker;

namespace MyDotnetFrameworkProject
{
    internal class Program
    {
        static void Main(string[] args)
        {
            FunctionsDebugger.Enable();

            var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults()
                .Build();

            host.Run();
        }
    }
}

接下来,需要使用 .NET Framework 调试程序手动附加到进程。 Visual Studio 目前不会自动对独立工作进程 .NET Framework 应用执行此操作,因此应避免“开始调试”操作。

在项目目录(或其生成输出目录)中,运行:

func host start --dotnet-isolated-debug

这会启动你的工作进程,然后该进程将停止,并显示以下消息:

Azure Functions .NET Worker (PID: <process id>) initialized in debug mode. Waiting for debugger to attach...

其中 <process id> 是你的工作进程的 ID。 现在可以使用 Visual Studio 手动附加到该进程。 有关此操作的说明,请参阅如何附加到正在运行的进程

附加调试器后,进程执行将会恢复,你将能够开始调试。

使用 Visual Studio 进行远程调试

由于独立工作进程应用在 Functions 运行时外部运行,因此你需要将远程调试器附加到单独的进程。 要详细了解如何使用 Visual Studio 进行调试,请参阅远程调试

后续步骤