Receber eventos em um ponto de extremidade HTTP
Este artigo descreve como validar um ponto de extremidade HTTP para receber eventos de uma Assinatura de Evento e, em seguida, receber e desserializar os eventos. Este artigo usa uma função do Azure para fins de demonstração. No entanto, os mesmos conceitos se aplicam independentemente de onde o aplicativo está hospedado.
Observação
Recomendamos que você use um Gatilho da Grade de Eventos ao acionar uma Função do Azure com a Grade de Eventos. Ele fornece uma integração mais fácil e rápida entre a Grade de Eventos e Azure Functions. No entanto, observe que o gatilho da Grade de Eventos do Azure Functions não dá suporte ao cenário em que o código hospedado precisa controlar o código de status HTTP retornado à Grade de Eventos. Dada essa limitação, seu código em execução em uma Função do Azure não poderá retornar um erro 5XX para iniciar uma nova tentativa de entrega de eventos pela Grade de Eventos, por exemplo.
Pré-requisitos
É necessário ter um aplicativo de funções com uma função disparada por HTTP.
Adicionar dependências
Se você estiver desenvolvendo em .NET, adicione uma dependência à sua função do Azure.Messaging.EventGrid
Pacote NuGet.
SDKs para outros idiomas estão disponíveis na referência SDKs de Publicação. Esses pacotes têm os modelos para tipos de eventos nativos, como EventGridEvent
, StorageBlobCreatedEventData
e EventHubCaptureFileCreatedEventData
.
Validação do ponto de extremidade
A primeira coisa a fazer é manipular eventos Microsoft.EventGrid.SubscriptionValidationEvent
. Sempre que alguém assina um evento, a Grade de Eventos envia um evento de validação para o ponto de extremidade com um validationCode
na carga de dados. O ponto de extremidade é necessário para ecoar no corpo da resposta a fim de provar que o ponto de extremidade é válido e pertence a você. Se você estiver usando um Gatilho da Grade de Eventos, em vez de uma Função disparada por WebHook, a validação do ponto de extremidade será tratada para você. Se você usar um serviço de API de terceiros (como Zapier ou IFTTT), você não poderá ecoar programaticamente o código de validação. Para esses serviços, você pode validar manualmente a assinatura usando uma URL de validação que é enviada no evento de validação de assinatura. Copie essa URL na propriedade validationUrl
e envie uma solicitação GET por meio de um cliente REST ou pelo navegador da web.
Em C#, o método ParseMany()
é usado para desserializar uma instância BinaryData
contendo um ou mais eventos em uma matriz de EventGridEvent
. Se souber com antecedência que está desserializando apenas um único evento, você pode usar o método Parse
.
Para ecoar programaticamente o código de validação, use o seguinte código.
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 System;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
namespace Function1
{
public static class Function1
{
[FunctionName("Function1")]
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 response = string.Empty;
BinaryData events = await BinaryData.FromStreamAsync(req.Body);
log.LogInformation($"Received events: {events}");
EventGridEvent[] eventGridEvents = EventGridEvent.ParseMany(events);
foreach (EventGridEvent eventGridEvent in eventGridEvents)
{
// Handle system events
if (eventGridEvent.TryGetSystemEventData(out object eventData))
{
// Handle the subscription validation event
if (eventData is SubscriptionValidationEventData subscriptionValidationEventData)
{
log.LogInformation($"Got SubscriptionValidation event data, validation code: {subscriptionValidationEventData.ValidationCode}, topic: {eventGridEvent.Topic}");
// Do any additional validation (as required) and then return back the below response
var responseData = new
{
ValidationResponse = subscriptionValidationEventData.ValidationCode
};
return new OkObjectResult(responseData);
}
}
}
return new OkObjectResult(response);
}
}
}
module.exports = function (context, req) {
context.log('JavaScript HTTP trigger function begun');
var validationEventType = "Microsoft.EventGrid.SubscriptionValidationEvent";
for (var events in req.body) {
var body = req.body[events];
// Deserialize the event data into the appropriate type based on event type
if (body.data && body.eventType == validationEventType) {
context.log("Got SubscriptionValidation event data, validation code: " + body.data.validationCode + " topic: " + body.topic);
// Do any additional validation (as required) and then return back the below response
var code = body.data.validationCode;
context.res = { status: 200, body: { "ValidationResponse": code } };
}
}
context.done();
};
Resposta de validação do teste
Teste a função de resposta de validação colando o evento de exemplo no campo de teste para a função:
[{
"id": "2d1781af-3a4c-4d7c-bd0c-e34b19da4e66",
"topic": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"subject": "",
"data": {
"validationCode": "512d38b6-c7b8-40c8-89fe-f46f9e9622b6"
},
"eventType": "Microsoft.EventGrid.SubscriptionValidationEvent",
"eventTime": "2018-01-25T22:12:19.4556811Z",
"metadataVersion": "1",
"dataVersion": "1"
}]
Quando você seleciona Executar, a Saída deve ser 200 OK e {"validationResponse":"512d38b6-c7b8-40c8-89fe-f46f9e9622b6"}
no corpo:
Tratar dos eventos do armazenamento de blobs
Agora vamos estender a função para manipular o evento do sistema Microsoft.Storage.BlobCreated
:
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 System;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
namespace Function1
{
public static class Function1
{
[FunctionName("Function1")]
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 response = string.Empty;
BinaryData events = await BinaryData.FromStreamAsync(req.Body);
log.LogInformation($"Received events: {events}");
EventGridEvent[] eventGridEvents = EventGridEvent.ParseMany(events);
foreach (EventGridEvent eventGridEvent in eventGridEvents)
{
// Handle system events
if (eventGridEvent.TryGetSystemEventData(out object eventData))
{
// Handle the subscription validation event
if (eventData is SubscriptionValidationEventData subscriptionValidationEventData)
{
log.LogInformation($"Got SubscriptionValidation event data, validation code: {subscriptionValidationEventData.ValidationCode}, topic: {eventGridEvent.Topic}");
// Do any additional validation (as required) and then return back the below response
var responseData = new
{
ValidationResponse = subscriptionValidationEventData.ValidationCode
};
return new OkObjectResult(responseData);
}
// Handle the storage blob created event
else if (eventData is StorageBlobCreatedEventData storageBlobCreatedEventData)
{
log.LogInformation($"Got BlobCreated event data, blob URI {storageBlobCreatedEventData.Url}");
}
}
}
return new OkObjectResult(response);
}
}
}
module.exports = function (context, req) {
context.log('JavaScript HTTP trigger function begun');
var validationEventType = "Microsoft.EventGrid.SubscriptionValidationEvent";
var storageBlobCreatedEvent = "Microsoft.Storage.BlobCreated";
for (var events in req.body) {
var body = req.body[events];
// Deserialize the event data into the appropriate type based on event type
if (body.data && body.eventType == validationEventType) {
context.log("Got SubscriptionValidation event data, validation code: " + body.data.validationCode + " topic: " + body.topic);
// Do any additional validation (as required) and then return back the below response
var code = body.data.validationCode;
context.res = { status: 200, body: { "ValidationResponse": code } };
}
else if (body.data && body.eventType == storageBlobCreatedEvent) {
var blobCreatedEventData = body.data;
context.log("Relaying received blob created event payload:" + JSON.stringify(blobCreatedEventData));
}
}
context.done();
};
Manipulação do evento Testar Blob Criado
Teste a nova funcionalidade da função colocando um evento de armazenamento de blobs no campo de teste e executando:
[{
"topic": "/subscriptions/{subscription-id}/resourceGroups/Storage/providers/Microsoft.Storage/storageAccounts/xstoretestaccount",
"subject": "/blobServices/default/containers/testcontainer/blobs/testfile.txt",
"eventType": "Microsoft.Storage.BlobCreated",
"eventTime": "2017-06-26T18:41:00.9584103Z",
"id": "aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e",
"data": {
"api": "PutBlockList",
"clientRequestId": "bbbb1b1b-cc2c-dd3d-ee4e-ffffff5f5f5f",
"requestId": "cccc2c2c-dd3d-ee4e-ff5f-aaaaaa6a6a6a",
"eTag": "0x8D4BCC2E4835CD0",
"contentType": "text/plain",
"contentLength": 524288,
"blobType": "BlockBlob",
"url": "https://example.blob.core.windows.net/testcontainer/testfile.txt",
"sequencer": "00000000000004420000000000028963",
"storageDiagnostics": {
"batchId": "dddd3d3d-ee4e-ff5f-aa6a-bbbbbb7b7b7b"
}
},
"dataVersion": "",
"metadataVersion": "1"
}]
Você deve ver a saída de URL do blob no log de função:
2022-11-14T22:40:45.978 [Information] Executing 'Function1' (Reason='This function was programmatically called via the host APIs.', Id=8429137d-9245-438c-8206-f9e85ef5dd61)
2022-11-14T22:40:46.012 [Information] C# HTTP trigger function processed a request.
2022-11-14T22:40:46.017 [Information] Received events: [{"topic": "/subscriptions/{subscription-id}/resourceGroups/Storage/providers/Microsoft.Storage/storageAccounts/xstoretestaccount","subject": "/blobServices/default/containers/testcontainer/blobs/testfile.txt","eventType": "Microsoft.Storage.BlobCreated","eventTime": "2017-06-26T18:41:00.9584103Z","id": "aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e","data": {"api": "PutBlockList","clientRequestId": "bbbb1b1b-cc2c-dd3d-ee4e-ffffff5f5f5f","requestId": "cccc2c2c-dd3d-ee4e-ff5f-aaaaaa6a6a6a","eTag": "0x8D4BCC2E4835CD0","contentType": "text/plain","contentLength": 524288,"blobType": "BlockBlob","url": "https://example.blob.core.windows.net/testcontainer/testfile.txt","sequencer": "00000000000004420000000000028963","storageDiagnostics": {"batchId": "dddd3d3d-ee4e-ff5f-aa6a-bbbbbb7b7b7b"}},"dataVersion": "","metadataVersion": "1"}]
2022-11-14T22:40:46.335 [Information] Got BlobCreated event data, blob URI https://example.blob.core.windows.net/testcontainer/testfile.txt
2022-11-14T22:40:46.346 [Information] Executed 'Function1' (Succeeded, Id=8429137d-9245-438c-8206-f9e85ef5dd61, Duration=387ms)
Você também pode testar criando uma conta de Armazenamento de Blobs ou uma conta de Armazenamento V2 de Uso Geral, adicionando uma assinatura de evento e definindo o ponto de extremidade para a URL da função:
Manipular eventos personalizados
Finalmente, vamos estender a função mais uma vez para que ela também possa tratar eventos personalizados.
Adicionar uma verificação para o evento Contoso.Items.ItemReceived
. O código final deve se assemelhar a:
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 System;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
namespace Function1
{
public static class Function1
{
[FunctionName("Function1")]
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 response = string.Empty;
BinaryData events = await BinaryData.FromStreamAsync(req.Body);
log.LogInformation($"Received events: {events}");
EventGridEvent[] eventGridEvents = EventGridEvent.ParseMany(events);
foreach (EventGridEvent eventGridEvent in eventGridEvents)
{
// Handle system events
if (eventGridEvent.TryGetSystemEventData(out object eventData))
{
// Handle the subscription validation event
if (eventData is SubscriptionValidationEventData subscriptionValidationEventData)
{
log.LogInformation($"Got SubscriptionValidation event data, validation code: {subscriptionValidationEventData.ValidationCode}, topic: {eventGridEvent.Topic}");
// Do any additional validation (as required) and then return back the below response
var responseData = new
{
ValidationResponse = subscriptionValidationEventData.ValidationCode
};
return new OkObjectResult(responseData);
}
// Handle the storage blob created event
else if (eventData is StorageBlobCreatedEventData storageBlobCreatedEventData)
{
log.LogInformation($"Got BlobCreated event data, blob URI {storageBlobCreatedEventData.Url}");
}
}
// Handle the custom contoso event
else if (eventGridEvent.EventType == "Contoso.Items.ItemReceived")
{
var contosoEventData = eventGridEvent.Data.ToObjectFromJson<ContosoItemReceivedEventData>();
log.LogInformation($"Got ContosoItemReceived event data, item SKU {contosoEventData.ItemSku}");
}
}
return new OkObjectResult(response);
}
}
}
module.exports = function (context, req) {
context.log('JavaScript HTTP trigger function begun');
var validationEventType = "Microsoft.EventGrid.SubscriptionValidationEvent";
var storageBlobCreatedEvent = "Microsoft.Storage.BlobCreated";
var customEventType = "Contoso.Items.ItemReceived";
for (var events in req.body) {
var body = req.body[events];
// Deserialize the event data into the appropriate type based on event type
if (body.data && body.eventType == validationEventType) {
context.log("Got SubscriptionValidation event data, validation code: " + body.data.validationCode + " topic: " + body.topic);
// Do any additional validation (as required) and then return back the below response
var code = body.data.validationCode;
context.res = { status: 200, body: { "ValidationResponse": code } };
}
else if (body.data && body.eventType == storageBlobCreatedEvent) {
var blobCreatedEventData = body.data;
context.log("Relaying received blob created event payload:" + JSON.stringify(blobCreatedEventData));
}
else if (body.data && body.eventType == customEventType) {
var payload = body.data;
context.log("Relaying received custom payload:" + JSON.stringify(payload));
}
}
context.done();
};
Testar tratamento de eventos personalizados
Por fim, teste se a função agora pode tratar o tipo de evento personalizado:
[{
"subject": "Contoso/foo/bar/items",
"eventType": "Contoso.Items.ItemReceived",
"eventTime": "2017-08-16T01:57:26.005121Z",
"id": "602a88ef-0001-00e6-1233-1646070610ea",
"data": {
"itemSku": "Standard"
},
"dataVersion": "",
"metadataVersion": "1"
}]
Você também pode testar essa funcionalidade ao vivo enviando um evento personalizado com CURL do Portal ou postando em um tópico personalizado usando qualquer serviço ou aplicativo que possa fazer POST em um ponto de extremidade. Crie um tópico personalizado e uma assinatura de evento com o ponto de extremidade definido como a URL de função.
Cabeçalhos de mensagem
Estas são as propriedades que você recebe nos cabeçalhos das mensagens:
Nome da propriedade | Descrição |
---|---|
aeg-subscription-name | Nome da assinatura do evento. |
aeg-delivery-count | Número de tentativas feitas para o evento. |
aeg-event-type | Tipo do evento. Pode ser um dos seguintes valores:
|
aeg-metadata-version | A versão de metadados do evento. Para o esquema de evento da Grade de Eventos, essa propriedade representa a versão de metadados e, para o esquema de evento da nuvem, ela representa a versão de especificação. |
aeg-data-version | Versão de dados do evento. Para o esquema de evento da Grade de Eventos, essa propriedade representa a versão dos dados e para o esquema de evento da nuvem, ela não se aplica. |
aeg-output-event-id | ID do evento da Grade de Eventos. |