Guia para executar o C# do Azure Functions em um processo isolado

Este artigo é uma introdução ao uso do C# para desenvolver funções de processo isolado do .NET, que são executadas fora do processo no Azure Functions. A execução fora do processo permite desacoplar seu código de função do tempo de execução do Azure Functions. As funções C# de processo isolado são executadas no .NET 6.0, no .NET 7.0 e no .NET Framework 4.8 (suporte para versão prévia). As funções da biblioteca de classes C# em processo não têm suporte no .NET 7.0.

Introdução Conceitos Exemplos

Por que o processo isolado do .NET?

Anteriormente o Azure Functions dava suporte apenas para um modo rigidamente integrado para funções do .NET, que são executadas como uma biblioteca de classes no mesmo processo que o host. Esse modo fornece uma integração profunda entre o processo de host e as funções. Por exemplo, as funções de biblioteca de classes do .NET podem compartilhar APIs e tipos de associação. No entanto, essa integração também requer um acoplamento mais rígido entre o processo de host e a função .NET. Por exemplo, as funções do .NET em execução no processo são necessárias para executar na mesma versão do .NET que o tempo de execução do Functions. Para permitir que você execute fora dessas restrições, agora você pode optar por executar em um processo isolado. Esse isolamento de processo também permite que você desenvolva funções que usam versões atuais do .NET (como o .NET 7.0), sem suporte nativo pelo runtime do Functions. As funções da biblioteca de classes C# em processo e de processo isolado são executadas no .NET 6.0. Para saber mais, consulte Versões compatíveis.

Como essas funções são executadas em um processo separado, há algumas diferenças de funcionalidade e recurso entre aplicativos de funções isoladas do .NET e aplicativos de função da biblioteca de classes do .NET.

Benefícios da execução fora do processo

Quando suas funções .NET são executadas fora do processo, você pode aproveitar os seguintes benefícios:

  • Menos conflitos: como as funções são executadas em um processo separado, os assemblies usados em seu aplicativo não entrarão em conflito com versões diferentes dos mesmos assemblies usados pelo processo de host.
  • Controle total do processo: você controla a inicialização do aplicativo e pode controlar as configurações usadas e o middleware iniciado.
  • Injeção de dependência: como você tem controle total do processo, você pode usar os comportamentos atuais do .NET para injeção de dependência e incorporar middleware em seu aplicativo de funções.

Versões com suporte

As versões runtime do Functions funcionam com versões específicas do .NET. Saiba mais sobre as versões do Functions, confira Visão geral de versões do Azure Functions runtime. O suporte à versão depende de suas funções executarem no processo ou fora do processo (isoladas).

Observação

Para saber como alterar a versão de runtime do Functions usada pelo aplicativo de funções, confira Exibir e atualizar a versão de runtime atual.

A tabela a seguir mostra o nível mais alto do .NET Core ou .NET Framework que pode ser usado com uma versão específica do Functions.

Versão do runtime do Functions Em processo
(Biblioteca de classes do .NET)
Fora do processo
(Isolado do .NET)
Functions 4.x .NET 6.0 .NET 6.0
.NET 7.0 (versão prévia)
.NET Framework 4.8 (versão prévia) 1
Functions 3.x .NET Core 3.1 .NET 5.02
Funções 2.x .NET Core 2.13 n/d
Funções 1.x .NET Framework 4.8 n/d

1O processo de build também requer o SDK do .NET 6. O suporte para .NET Framework 4.8 está em versão prévia.

2O processo de build também requer o SDK do .NET Core 3.1.

3 Para saber mais, confira Considerações sobre o Functions v2.x.

Para receber as notícias mais recentes sobre as versões de Azure Functions, incluindo a remoção de versões secundárias específicas mais antigas, acompanhe os comunicados de Serviço de Aplicativo do Azure.

Projeto isolado do .NET

Um projeto de função isolado do .NET é basicamente um projeto de aplicativo de console .NET que tem como alvo um runtime .NET com suporte. Estes são os arquivos básicos necessários em qualquer projeto isolado do .NET:

Para obter exemplos completos, consulte o projeto de exemplo isolado do .NET 6 e o projeto de exemplo isolado do .NET Framework 4.8.

Observação

Para poder publicar seu projeto de função isolada em um aplicativo de funções Windows ou Linux no Azure, você precisa definir um valor de dotnet-isolated na configuração de aplicativo remoto FUNCTIONS_WORKER_RUNTIME. Para dar suporte à implantação zip e à execução do pacote de implantação no Linux, também será necessário atualizar a definição de configuração do site linuxFxVersion para DOTNET-ISOLATED|7.0. Para saber mais, consulte Atualizações manuais de versão no Linux.

Referências de pacote

Quando suas funções são executadas fora do processo, seu projeto do .NET usa um conjunto exclusivo de pacotes, que implementam a funcionalidade principal e as extensões de associação.

Pacotes principais

Os pacotes a seguir são necessários para executar as funções do .NET em um processo isolado:

Pacotes de extensão

Como as funções que são executadas em um processo isolado do .NET usam tipos de associação diferentes, elas exigem um conjunto exclusivo de pacotes de extensão de associação.

Você encontrará esses pacotes de extensão em Microsoft.Azure.Functions.Worker.Extensions.

Inicialização e configuração

Ao usar funções isoladas do .NET, você tem acesso à inicialização do seu aplicativo de funções, que geralmente está em Program.cs. Você é responsável por criar e iniciar sua própria instância de host. Assim, você também tem acesso direto ao pipeline de configuração para seu aplicativo. Quando você executa suas funções fora do processo, pode adicionar configurações com mais facilidade, injetar dependências e executar seu próprio middleware.

O código a seguir mostra um exemplo de um pipeline HostBuilder:

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

Este código requer using Microsoft.Extensions.DependencyInjection;.

Um HostBuilder é usado para criar e retornar uma instância de IHost totalmente inicializada, que você executa de forma assíncrona para iniciar seu aplicativo de funções.

await host.RunAsync();

Importante

Se o projeto for direcionado para o .NET Framework 4.8, você também precisará adicionar FunctionsDebugger.Enable(); antes de criar o HostBuilder. Essa deve ser a primeira linha de seu método Main(). Consulte Depuração ao direcionar o .NET Framework para obter mais informações.

Configuração

O método ConfigureFunctionsWorkerDefaults é usado para adicionar as configurações necessárias para que o aplicativo de funções seja executado fora do processo, o que inclui a seguinte funcionalidade:

  • Conjunto padrão de conversores.
  • Defina o JsonSerializerOptions padrão para ignorar maiúsculas e minúsculas nos nomes de propriedade.
  • Integrar com o log de Azure Functions.
  • Middleware de associação de saída e recursos.
  • Middleware de execução de função.
  • Suporte a gRPC padrão.
.ConfigureFunctionsWorkerDefaults(builder =>
{
    builder
        .AddApplicationInsights()
        .AddApplicationInsightsLogger();
})

Ter acesso ao pipeline do host Builder significa que você também pode definir qualquer configuração específica do aplicativo durante a inicialização. Você pode chamar o método ConfigureAppConfiguration em HostBuilder uma ou mais vezes para adicionar as configurações exigidas por seu aplicativo de funções. Para obter mais informações sobre a configuração de aplicativos, consulte Configuração no ASP.NET Core.

Essas configurações se aplicam ao seu aplicativo de funções em execução em um processo separado. Para fazer alterações no host do Functions ou no gatilho e na configuração de associação, você ainda precisará usar o arquivo host.json.

Injeção de dependência

A injeção de dependência é simplificada, em comparação com as bibliotecas de classes do .NET. Em vez de precisar criar uma classe de inicialização para registrar serviços, basta chamar ConfigureServiceservices no construtor de hosts e usar os métodos de extensão em IServiceCollection para injetar serviços específicos.

O exemplo a seguir injeta uma dependência de serviço singleton:

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

Este código requer using Microsoft.Extensions.DependencyInjection;. Para saber mais, confira Injeção de dependência em ASP.NET Core.

Middleware

O .NET isolado também dá suporte ao registro de middleware, novamente usando um modelo semelhante ao que existe em ASP.NET. Esse modelo oferece a capacidade de injetar a lógica no pipeline de invocação e as funções before e after são executadas.

O método de extensão ConfigureFunctionsWorkerDefaults tem uma sobrecarga que permite registrar seu próprio middleware, como você pode ver no exemplo a seguir.

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();

O método de extensão UseWhen pode ser usado para registrar um middleware que é executado condicionalmente. Um predicado que retorna um valor booliano precisa ser passado para esse método e o middleware participará do pipeline de processamento de invocação se o valor retornado pelo predicado for verdadeiro.

Os métodos de extensão a seguir no FunctionContext facilitam o trabalho com middleware no modelo isolado.

Método Descrição
GetHttpRequestDataAsync Obtém a instância de HttpRequestData quando chamada por um gatilho HTTP. Esse método retorna uma instância de ValueTask<HttpRequestData?>, o que é útil quando você deseja ler dados de mensagem, como cookies e cabeçalhos de solicitação.
GetHttpResponseData Obtém a instância de HttpResponseData quando chamada por um gatilho HTTP.
GetInvocationResult Obtém uma instância de InvocationResult, que representa o resultado da execução da função atual. Use a propriedade Value para obter ou definir o valor conforme necessário.
GetOutputBindings Obtém as entradas de associação de saída para a execução da função atual. Cada entrada no resultado desse método é do tipo OutputBindingData. Você pode usar a propriedade Value para obter ou definir o valor conforme necessário.
BindInputAsync Associa um item de associação de entrada para a instância de BindingMetadata solicitada. Por exemplo, você poderá usar esse método quando tiver uma função com uma associação de entrada BlobInput que precisa ser acessada ou atualizada pelo middleware.

Veja a seguir um exemplo de uma implementação de middleware que lê a instância de HttpRequestData e atualiza a instância de HttpResponseData durante a execução da função. Esse middleware verifica a presença de um cabeçalho de solicitação específico (x-correlationId) e, quando presente, usa o valor do cabeçalho para carimbar um cabeçalho de resposta. Caso contrário, ele gera um novo valor de GUID e o utiliza para carimbar o cabeçalho de resposta.

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);
    }
}

Para obter um exemplo mais completo de como usar o middleware personalizado em seu aplicativo de funções, consulte o exemplo de referência de middleware personalizado.

Contexto de execução

O .NET isolado passa um objeto FunctionContext para seus métodos de função. Esse objeto permite que você obtenha uma instância de ILogger para gravar nos logs chamando o método GetLogger e fornecendo uma categoryName cadeia de caracteres. Para saber mais, consulte Logging.

Associações

As associações são definidas usando atributos em métodos, parâmetros e tipos de retorno. Um método de função é um método com um atributo Function e um atributo de gatilho aplicado a um parâmetro de entrada, conforme mostrado no exemplo a seguir:

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

    FunctionContext context)

O atributo de gatilho especifica o tipo de gatilho e associa dados de entrada a um parâmetro de método. A função de exemplo é disparada por uma mensagem de fila, a qual é transmitida para o método no parâmetro myQueueItem.

O atributo Function marca o método como um ponto de entrada da função. O nome deve ser exclusivo dentro de um projeto, começar com uma letra e conter apenas letras, números, _ e -, até 127 caracteres. Modelos de projeto geralmente criam um método chamado Run, mas o nome do método pode ser qualquer nome de método C# válido.

Como os projetos isolados do .NET são executados em um processo de trabalho separado, as associações não podem tirar proveito das classes de associação avançadas, como, ICollector<T>IAsyncCollector<T> e CloudBlockBlob. Também não há suporte direto para tipos herdados de SDKs de serviço subjacentes, como DocumentClient e BrokeredMessage. Em vez disso, as associações dependem de cadeias de caracteres, matrizes e tipos serializáveis, como POCOs (objetos de classe antiga).

Para gatilhos HTTP, você deve usar HttpRequestData e HttpResponseData para acessar os dados de solicitação e resposta. Isso ocorre porque você não tem acesso aos objetos originais de solicitação e resposta HTTP durante a execução fora do processo.

Para obter um conjunto completo de exemplos de referência para usar gatilhos e associações ao executar fora do processo, consulte o exemplo de referência de extensões de associação.

Associações de entrada

Uma função pode ter zero ou mais associações de entrada que podem passar dados para uma função. Como gatilhos, as associações de entrada são definidas pela aplicação de um atributo de associação a um parâmetro de entrada. Quando a função é executada, o tempo de execução tenta obter os dados especificados na associação. Os dados que estão sendo solicitados geralmente dependem das informações fornecidas pelo gatilho usando parâmetros de associação.

Associações de saída

Para gravar em uma associação de saída, você deve aplicar um atributo de associação de saída ao método de função, que definiu como gravar no serviço associado. O valor retornado pelo método é gravado na associação de saída. Por exemplo, o exemplo a seguir grava um valor de cadeia de caracteres em uma fila de mensagens chamada myqueue-output usando uma associação de saída:

[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;
}

Várias associações de saída

Os dados gravados em uma associação de saída são sempre o valor de retorno da função. Se você precisar gravar em mais de uma associação de saída, deverá criar um tipo de retorno personalizado. Esse tipo de retorno deve ter o atributo de associação de saída aplicado a uma ou mais propriedades da classe. O exemplo a seguir de um gatilho HTTP é gravado na resposta HTTP e em uma associação de saída de fila:

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; }
}

A resposta de um gatilho HTTP é sempre considerada uma saída, portanto, um atributo de valor de retorno não é necessário.

Gatilho HTTP

Os gatilhos HTTP convertem a mensagem de solicitação HTTP de entrada em um objeto HttpRequestData que é passado para a função. Esse objeto fornece dados da solicitação, incluindo Headers, Cookies, Identities, URL e uma mensagem opcional Body. Esse objeto é uma representação do objeto de solicitação HTTP e não da solicitação em si.

Da mesma forma, a função retorna um objeto HttpResponseData, que fornece dados usados para criar a resposta HTTP, incluindo a mensagem StatusCode, Headers e, opcionalmente, uma mensagem Body.

O código a seguir é um gatilho 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;
}

Log

No .NET isolado, você pode gravar em logs usando uma instância ILogger obtida de um objeto FunctionContext passado para sua função. Chame o método GetLogger, passando um valor de cadeia de caracteres que é o nome da categoria na qual os logs são gravados. A categoria geralmente é o nome da função específica da qual os logs são gravados. Para saber mais sobre categorias, consulte o artigo monitoramento.

O exemplo a seguir mostra como obter um ILogger e gravar logs dentro de uma função:

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

Use vários métodos de ILogger para gravar vários níveis de log, como LogWarning ou LogError. Para saber mais sobre os níveis de log, consulte o artigo monitoramento.

Um ILogger também é fornecido ao usar a injeção de dependência.

Depuração ao direcionar o .NET Framework

Se o projeto isolado for direcionado ao .NET Framework 4.8, o escopo de visualização atual exigirá etapas manuais para habilitar a depuração. Essas etapas não serão necessárias se outra estrutura de destino estiver sendo usada.

Seu aplicativo deve começar com uma chamada para FunctionsDebugger.Enable(); como a primeira operação. Isso ocorre no método Main() antes de inicializar um HostBuilder. O arquivo Program.cs deve ser semelhante ao seguinte:

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();
        }
    }
}

Em seguida, você precisa anexar manualmente ao processo usando um depurador de .NET Framework. O Visual Studio ainda não faz isso automaticamente para aplicativos .NET Framework de processo isolado, e a operação "Iniciar Depuração" deve ser evitada.

No diretório do projeto (ou no diretório de saída de build), execute:

func host start --dotnet-isolated-debug

Isso iniciará seu trabalho e o processo será interrompido com a seguinte mensagem:

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

Em que <process id> é a ID do processo de trabalho. Agora, você pode usar Visual Studio para anexar manualmente ao processo. Para obter instruções sobre essa operação, consulte Como anexar a um processo em execução.

Depois que o depurador for anexado, a execução do processo será retomada e você poderá depurar.

Diferenças com funções de biblioteca de classes do .NET

Esta seção descreve o estado atual das diferenças funcionais e comportamentais em execução fora do processo em comparação com as funções da biblioteca de classes do .NET em execução no processo:

Recurso/comportamento Em processo Fora do processo
Versões do .NET .NET Core 3.1
.NET 6.0
.NET 6.0
.NET 7.0 (versão prévia)
.NET Framework 4.8 (versão prévia)
Pacotes principais Microsoft.NET.Sdk.Functions Microsoft.Azure.Functions.Worker
Microsoft.Azure.Functions.Worker.Sdk
Pacotes de extensão de associação Microsoft.Azure.WebJobs.Extensions.* Microsoft.Azure.Functions.Worker.Extensions.*
Funções duráveis Com suporte Com suporte (visualização pública)
Tipos de modelo expostos por associações Tipos simples
Tipos serializáveis JSON
Matrizes/enumerações
Tipos de SDK de serviço, como BlobClient
IAsyncCollector (para associações de saída)
Tipos simples
Tipos serializáveis JSON
Matrizes/enumerações
Tipos de modelo de gatilho HTTP HttpRequest/ObjectResult HttpRequestData/HttpResponseData
Interação de associação de saída Valores retornados (somente saída única)
Parâmetros out
IAsyncCollector
Valores retornados (modelo expandido com saídas múltiplas ou únicas)
Associações imperativas1 Com suporte Sem suporte
Injeção de dependência Com suporte Com suporte
Middleware Sem suporte Com suporte
Log ILogger passado para a função
ILogger<T> por meio da injeção de dependência
ILogger/ILogger<T> obtido de FunctionContext ou por meio da injeção de dependência
Dependências do Application Insights Com suporte Com suporte (visualização pública)
Tokens de cancelamento Com suporte Sem suporte
Horários de inicialização a frio2 (Linha de Base) Além disso, inclui a inicialização do processo
ReadyToRun Com suporte TBD

1 Quando você precisa interagir com um serviço usando parâmetros determinados em runtime, o uso de SDKs de serviço correspondentes diretamente é recomendado com associações imperativas. Os SDKs são menos detalhados, cobrem mais cenários e têm vantagens para fins de tratamento de erro e depuração. Essa recomendação se aplica a ambos os modelos.

2 Horários de inicialização a frio podem ser afetados adicionalmente no Windows ao usar algumas versões prévias do .NET devido ao carregamento just-in-time de estruturas de visualização. Isso se aplica aos modelos em processo e fora de processo, mas pode ser particularmente perceptível se comparado entre versões diferentes. Esse atraso para versões prévias não está presente nos planos do Linux.

Depuração remota usando o Visual Studio

Como seu aplicativo de processo isolado é executado fora do runtime do Functions, você precisa anexar o depurador remoto a um processo separado. Para saber mais sobre a depuração usando o Visual Studio, confira Depuração Remota.

Próximas etapas